From 6fd75f6cefe419f463e79c6aa9956a9b719599d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E7=9A=93?= Date: Thu, 23 Nov 2023 20:36:43 -0500 Subject: [PATCH] Implement collision preview (#55) * Add collision shader This adds a new shader for collision that renders over the fog. It doesn't do anything yet because the collision is not being calculated anywhere yet. (cherry picked from commit eeecc6cda340ed83a032182c67396969b021ecf6) * Fix loop dimensions in collision shader `calculate_vertices` (cherry picked from commit 93bc2c93518d6d3d2221ad50877a6ba0a724c434) * Render fog and collision on top of event graphics I also made the event border rectangles render on top of the fog and collision because otherwise they may be hard to see through those layers. (cherry picked from commit ade0fa2a1ae36d8c42c56f985b46798b498f8331) * Start implementing collision calculation (cherry picked from commit 7938c2226048132471a74667e37f17bc5b621365) * Replace image cache with dummy bind group in collision shader (cherry picked from commit cbbf696d102af03d83edcd9a50ac0ce9645181c9) * Update collision preview when map changes (cherry picked from commit 2ad9e87a46c136821391e7d8f707fb98894b3bb9) * Fix fog and collision not rendering when events are disabled (cherry picked from commit b47e70b8eff64e0905d3aa77217eacbee4596b14) * Add collision preview to tilepicker as well (cherry picked from commit c5dd6d01ea2e99951c3e6f9c7630bb05f0809331) * Collision preview now ignores disabled map layers (cherry picked from commit 28e17bfac9b4d755b843afc1a0b62e9402dc659c) * Fix labels in layer picker not aligning with checkboxes (cherry picked from commit e7770573d97ba17c466639e29d5439c07781a4d3) * Bind viewport to group 0 instead of using dummy layout (cherry picked from commit 74874f2f552d8b0b4137d174b805884ed521cc92) * Fix crash when map has tile IDs larger than tileset size * Fix crash when tileset `passages` is smaller than the tileset * Use `copy_from_slice` to calculate tilepicker passages * Remove extra vertices from collision shader * Collision shader `calculate_vertices` now returns an array instead of a vector * Fix collision calculation edge cases * Simplify `calculate_passage` --- crates/components/src/map_view.rs | 49 +++-- crates/components/src/tilepicker.rs | 48 ++++- crates/graphics/src/collision/collision.wgsl | 50 +++++ crates/graphics/src/collision/instance.rs | 196 +++++++++++++++++++ crates/graphics/src/collision/mod.rs | 190 ++++++++++++++++++ crates/graphics/src/collision/shader.rs | 105 ++++++++++ crates/graphics/src/collision/vertex.rs | 36 ++++ crates/graphics/src/event.rs | 2 +- crates/graphics/src/lib.rs | 6 + crates/graphics/src/map.rs | 75 ++++++- crates/graphics/src/tiles/atlas.rs | 6 +- crates/graphics/src/tiles/tilemap.wgsl | 2 +- crates/graphics/src/viewport.rs | 8 +- crates/ui/src/tabs/map/mod.rs | 113 ++++++++--- 14 files changed, 834 insertions(+), 52 deletions(-) create mode 100644 crates/graphics/src/collision/collision.wgsl create mode 100644 crates/graphics/src/collision/instance.rs create mode 100644 crates/graphics/src/collision/mod.rs create mode 100644 crates/graphics/src/collision/shader.rs create mode 100644 crates/graphics/src/collision/vertex.rs diff --git a/crates/components/src/map_view.rs b/crates/components/src/map_view.rs index 90912bb6..d45a0870 100644 --- a/crates/components/src/map_view.rs +++ b/crates/components/src/map_view.rs @@ -50,6 +50,10 @@ pub struct MapView { pub scale: f32, pub previous_scale: f32, + + /// Used to store the bounding boxes of event graphics in order to render them on top of the + /// fog and collision layers + pub event_rects: Vec, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Default)] @@ -70,6 +74,16 @@ impl MapView { let tilesets = update_state.data.tilesets(); let tileset = &tilesets[map.tileset_id]; + let mut passages = luminol_data::Table2::new(map.data.xsize(), map.data.ysize()); + luminol_graphics::collision::calculate_passages( + &tileset.passages, + &tileset.priorities, + &map.data, + Some(&map.events), + (0..map.data.zsize()).rev(), + |x, y, passage| passages[(x, y)] = passage, + ); + let atlas = update_state.graphics.atlas_cache.load_atlas( &update_state.graphics, update_state.filesystem, @@ -112,6 +126,7 @@ impl MapView { update_state.filesystem, &map, tileset, + &passages, update_state.graphics.push_constants_supported(), )?; @@ -139,6 +154,8 @@ impl MapView { scale: 100., previous_scale: 100., + + event_rects: Vec::new(), }) } @@ -335,7 +352,7 @@ impl MapView { if self.event_enabled { let mut selected_event = None; - let mut selected_event_rects = None; + let mut selected_event_rect = None; for (_, event) in map.events.iter() { let sprites = self.events.get(event.id); @@ -389,11 +406,7 @@ impl MapView { if matches!(self.selected_layer, SelectedLayer::Events) && ui.input(|i| !i.modifiers.shift) { - ui.painter().rect_stroke( - box_rect, - 5., - egui::Stroke::new(1., egui::Color32::WHITE), - ); + self.event_rects.push(box_rect); // If the mouse is not hovering over an event, then we will handle the selected // tile based on where the map cursor is @@ -421,7 +434,7 @@ impl MapView { }; if let Some(e) = selected_event { if e.id == event.id { - selected_event_rects = Some(box_rect); + selected_event_rect = Some(box_rect); } } } @@ -503,7 +516,7 @@ impl MapView { if let Some(e) = selected_event { if e.id == event.id { self.selected_event_is_hovered = true; - selected_event_rects = Some(box_rect); + selected_event_rect = Some(box_rect); } } } @@ -514,7 +527,7 @@ impl MapView { if let Some(id) = self.selected_event_id { if dragging_event && id == event.id { selected_event = Some(event); - selected_event_rects = Some(box_rect); + selected_event_rect = Some(box_rect); } } } else { @@ -537,20 +550,34 @@ impl MapView { self.selected_event_id = selected_event.map(|e| e.id); + // Draw the fog and collision layers + self.map + .paint_overlay(graphics_state.clone(), ui.painter(), canvas_rect); + + // Draw white rectangles on the border of all events + while let Some(rect) = self.event_rects.pop() { + ui.painter() + .rect_stroke(rect, 5., egui::Stroke::new(1., egui::Color32::WHITE)); + } + // Draw a yellow rectangle on the border of the selected event's graphic if let Some(selected_event) = selected_event { // Make sure the event editor isn't open so we don't draw over the // magenta rectangle if !selected_event.extra_data.is_editor_open { - if let Some(box_rect) = selected_event_rects { + if let Some(rect) = selected_event_rect { ui.painter().rect_stroke( - box_rect, + rect, 5., egui::Stroke::new(3., egui::Color32::YELLOW), ); } } } + } else { + // Draw the fog and collision layers + self.map + .paint_overlay(graphics_state.clone(), ui.painter(), canvas_rect); } // Do we display the visible region? diff --git a/crates/components/src/tilepicker.rs b/crates/components/src/tilepicker.rs index 688d9622..2c86bb4b 100644 --- a/crates/components/src/tilepicker.rs +++ b/crates/components/src/tilepicker.rs @@ -36,12 +36,15 @@ pub struct Tilepicker { #[derive(Debug)] struct Resources { tiles: luminol_graphics::tiles::Tiles, + collision: luminol_graphics::collision::Collision, viewport: luminol_graphics::viewport::Viewport, } struct Callback { resources: Arc, graphics_state: Arc, + + coll_enabled: bool, } // FIXME @@ -51,11 +54,11 @@ unsafe impl Sync for Callback {} impl egui_wgpu::CallbackTrait for Callback { fn paint<'a>( &'a self, - info: egui::PaintCallbackInfo, + _info: egui::PaintCallbackInfo, render_pass: &mut wgpu::RenderPass<'a>, - callback_resources: &'a egui_wgpu::CallbackResources, + _callback_resources: &'a egui_wgpu::CallbackResources, ) { - self.resources.viewport.bind(render_pass); + self.resources.viewport.bind(1, render_pass); self.resources.tiles.draw( &self.graphics_state, &self.resources.viewport, @@ -63,6 +66,15 @@ impl egui_wgpu::CallbackTrait for Callback { None, render_pass, ); + + if self.coll_enabled { + self.resources.viewport.bind(0, render_pass); + self.resources.collision.draw( + &self.graphics_state, + &self.resources.viewport, + render_pass, + ); + } } } @@ -145,8 +157,34 @@ impl Tilepicker { update_state.graphics.push_constants_supported(), ); + 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, + &passages, + update_state.graphics.push_constants_supported(), + ); + Ok(Self { - resources: Arc::new(Resources { tiles, viewport }), + resources: Arc::new(Resources { + tiles, + collision, + viewport, + }), ani_time: None, selected_tiles_left: 0, selected_tiles_top: 0, @@ -172,6 +210,7 @@ impl Tilepicker { update_state: &luminol_core::UpdateState<'_>, ui: &mut egui::Ui, scroll_rect: egui::Rect, + coll_enabled: bool, ) -> egui::Response { let time = ui.ctx().input(|i| i.time); let graphics_state = update_state.graphics.clone(); @@ -219,6 +258,7 @@ impl Tilepicker { Callback { resources: self.resources.clone(), graphics_state: graphics_state.clone(), + coll_enabled, }, )); diff --git a/crates/graphics/src/collision/collision.wgsl b/crates/graphics/src/collision/collision.wgsl new file mode 100644 index 00000000..5e46e76b --- /dev/null +++ b/crates/graphics/src/collision/collision.wgsl @@ -0,0 +1,50 @@ +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/instance.rs b/crates/graphics/src/collision/instance.rs new file mode 100644 index 00000000..28d354d3 --- /dev/null +++ b/crates/graphics/src/collision/instance.rs @@ -0,0 +1,196 @@ +// 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)] +struct Instance { + position: [f32; 3], + passage: u32, +} + +impl Instances { + pub fn new(render_state: &egui_wgpu::RenderState, passages: &luminol_data::Table2) -> Self { + let instances = Self::calculate_instances(passages); + let instance_buffer = + render_state + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("tilemap collision instance buffer"), + contents: bytemuck::cast_slice(&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(), + } + } + + pub fn set_passage( + &self, + render_state: &egui_wgpu::RenderState, + passage: i16, + position: (usize, usize), + ) { + let offset = position.0 + (position.1 * self.map_width); + let offset = offset * std::mem::size_of::(); + render_state.queue.write_buffer( + &self.instance_buffer, + offset as wgpu::BufferAddress, + bytemuck::bytes_of(&Instance { + position: [position.0 as f32, position.1 as f32, 0.0], + passage: passage as u32, + }), + ) + } + + fn calculate_instances(passages: &luminol_data::Table2) -> Vec { + passages + .iter() + .copied() + .enumerate() + .map(|(index, passage)| { + // We reset the x every xsize elements. + let map_x = index % passages.xsize(); + // We reset the y every ysize elements, but only increment it every xsize elements. + 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 + ], + 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; + 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..12, 0..count); + } + + pub const fn desc() -> wgpu::VertexBufferLayout<'static> { + const ARRAY: &[wgpu::VertexAttribute] = + &wgpu::vertex_attr_array![2 => Float32x3, 3 => Uint32]; + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: ARRAY, + } + } +} diff --git a/crates/graphics/src/collision/mod.rs b/crates/graphics/src/collision/mod.rs new file mode 100644 index 00000000..32ef7afb --- /dev/null +++ b/crates/graphics/src/collision/mod.rs @@ -0,0 +1,190 @@ +// 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 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 use_push_constants: bool, +} + +#[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 Some(page) = event.pages.first() else { + return None; + }; + 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: &crate::GraphicsState, + passages: &luminol_data::Table2, + use_push_constants: bool, + ) -> Self { + let instances = Instances::new(&graphics_state.render_state, &passages); + + Self { + instances, + use_push_constants, + } + } + + pub fn set_passage( + &self, + render_state: &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 crate::GraphicsState, + viewport: &crate::viewport::Viewport, + render_pass: &mut wgpu::RenderPass<'rpass>, + ) { + #[repr(C)] + #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] + struct VertexPushConstant { + viewport: [u8; 64], + } + + render_pass.push_debug_group("tilemap collision renderer"); + render_pass.set_pipeline(&graphics_state.pipelines.collision); + if self.use_push_constants { + render_pass.set_push_constants( + wgpu::ShaderStages::VERTEX, + 0, + bytemuck::bytes_of(&VertexPushConstant { + viewport: viewport.as_bytes(), + }), + ); + } + self.instances.draw(render_pass); + render_pass.pop_debug_group(); + } +} diff --git a/crates/graphics/src/collision/shader.rs b/crates/graphics/src/collision/shader.rs new file mode 100644 index 00000000..63d569a8 --- /dev/null +++ b/crates/graphics/src/collision/shader.rs @@ -0,0 +1,105 @@ +// 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::instance::Instances; +use super::Vertex; + +pub fn create_render_pipeline( + render_state: &egui_wgpu::RenderState, + bind_group_layouts: &crate::BindGroupLayouts, +) -> 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 result = composer.make_naga_module(naga_oil::compose::NagaModuleDescriptor { + source: include_str!("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), + )]), + 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 + .create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Tilemap Collision Shader Module"), + source: wgpu::ShaderSource::Naga(std::borrow::Cow::Owned(module)), + }); + + let pipeline_layout = if crate::push_constants_supported(render_state) { + render_state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Tilemap Collision Render Pipeline Layout (push constants)"), + bind_group_layouts: &[], + push_constant_ranges: &[ + // Viewport + wgpu::PushConstantRange { + stages: wgpu::ShaderStages::VERTEX, + range: 0..64, + }, + ], + }) + } else { + render_state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Tilemap Collision Render Pipeline Layout (uniforms)"), + bind_group_layouts: &[&bind_group_layouts.viewport], + push_constant_ranges: &[], + }) + }; + + render_state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Tilemap Collision Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader_module, + entry_point: "vs_main", + buffers: &[Vertex::desc(), Instances::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader_module, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + ..render_state.target_format.into() + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }) +} diff --git a/crates/graphics/src/collision/vertex.rs b/crates/graphics/src/collision/vertex.rs new file mode 100644 index 00000000..f40bd2e0 --- /dev/null +++ b/crates/graphics/src/collision/vertex.rs @@ -0,0 +1,36 @@ +// 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 . + +#[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, +} + +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, + } + } +} diff --git a/crates/graphics/src/event.rs b/crates/graphics/src/event.rs index c087eb1d..d627eac7 100644 --- a/crates/graphics/src/event.rs +++ b/crates/graphics/src/event.rs @@ -45,7 +45,7 @@ impl egui_wgpu::CallbackTrait for Callback { render_pass: &mut wgpu::RenderPass<'a>, callback_resources: &'a egui_wgpu::CallbackResources, ) { - self.resources.viewport.bind(render_pass); + self.resources.viewport.bind(1, render_pass); self.resources .sprite .draw(&self.graphics_state, &self.resources.viewport, render_pass); diff --git a/crates/graphics/src/lib.rs b/crates/graphics/src/lib.rs index 4c7156c2..a64a756a 100644 --- a/crates/graphics/src/lib.rs +++ b/crates/graphics/src/lib.rs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with Luminol. If not, see . +pub mod collision; pub mod quad; pub mod sprite; pub mod tiles; @@ -52,6 +53,7 @@ pub struct BindGroupLayouts { pub struct Pipelines { sprites: std::collections::HashMap, tiles: wgpu::RenderPipeline, + collision: wgpu::RenderPipeline, } impl GraphicsState { @@ -67,6 +69,10 @@ impl GraphicsState { 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, + ), }; let image_cache = image_cache::Cache::default(); diff --git a/crates/graphics/src/map.rs b/crates/graphics/src/map.rs index 67721a5c..15390308 100644 --- a/crates/graphics/src/map.rs +++ b/crates/graphics/src/map.rs @@ -27,6 +27,7 @@ pub struct Map { pub fog_enabled: bool, pub pano_enabled: bool, + pub coll_enabled: bool, pub enabled_layers: Vec, } @@ -36,30 +37,40 @@ struct Resources { viewport: crate::viewport::Viewport, panorama: Option, fog: Option, + collision: crate::collision::Collision, } struct Callback { resources: Arc, graphics_state: Arc, - fog_enabled: bool, pano_enabled: bool, enabled_layers: Vec, selected_layer: Option, } +struct OverlayCallback { + resources: Arc, + graphics_state: Arc, + + fog_enabled: bool, + coll_enabled: bool, +} + // FIXME unsafe impl Send for Callback {} unsafe impl Sync for Callback {} +unsafe impl Send for OverlayCallback {} +unsafe impl Sync for OverlayCallback {} impl egui_wgpu::CallbackTrait for Callback { fn paint<'a>( &'a self, - info: egui::PaintCallbackInfo, + _info: egui::PaintCallbackInfo, render_pass: &mut wgpu::RenderPass<'a>, - callback_resources: &'a egui_wgpu::CallbackResources, + _callback_resources: &'a egui_wgpu::CallbackResources, ) { - self.resources.viewport.bind(render_pass); + self.resources.viewport.bind(1, render_pass); if self.pano_enabled { if let Some(panorama) = &self.resources.panorama { @@ -74,11 +85,32 @@ impl egui_wgpu::CallbackTrait for Callback { self.selected_layer, render_pass, ); + } +} + +impl egui_wgpu::CallbackTrait for OverlayCallback { + fn paint<'a>( + &'a self, + _info: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'a>, + _callback_resources: &'a egui_wgpu::CallbackResources, + ) { + self.resources.viewport.bind(1, render_pass); + if self.fog_enabled { if let Some(fog) = &self.resources.fog { fog.draw(&self.graphics_state, &self.resources.viewport, render_pass); } } + + if self.coll_enabled { + self.resources.viewport.bind(0, render_pass); + self.resources.collision.draw( + &self.graphics_state, + &self.resources.viewport, + render_pass, + ); + } } } @@ -88,6 +120,7 @@ impl Map { filesystem: &impl luminol_filesystem::FileSystem, map: &luminol_data::rpg::Map, tileset: &luminol_data::rpg::Tileset, + passages: &luminol_data::Table2, use_push_constants: bool, ) -> anyhow::Result { let atlas = graphics_state @@ -95,6 +128,8 @@ impl Map { .load_atlas(graphics_state, filesystem, tileset)?; let tiles = crate::tiles::Tiles::new(graphics_state, atlas, &map.data, use_push_constants); + let collision = + crate::collision::Collision::new(graphics_state, passages, use_push_constants); let panorama = if let Some(ref panorama_name) = tileset.panorama_name { Some(Plane::new( @@ -155,12 +190,14 @@ impl Map { viewport, panorama, fog, + collision, }), ani_time: None, fog_enabled: true, pano_enabled: true, + coll_enabled: false, enabled_layers: vec![true; map.data.zsize()], }) } @@ -176,6 +213,17 @@ impl Map { .set_tile(render_state, tile_id, position); } + pub fn set_passage( + &self, + render_state: &egui_wgpu::RenderState, + passage: i16, + position: (usize, usize), + ) { + self.resources + .collision + .set_passage(render_state, passage, position); + } + pub fn set_proj(&self, render_state: &egui_wgpu::RenderState, proj: glam::Mat4) { self.resources.viewport.set_proj(render_state, proj); } @@ -210,11 +258,28 @@ impl Map { resources: self.resources.clone(), graphics_state, - fog_enabled: self.fog_enabled, pano_enabled: self.pano_enabled, enabled_layers: self.enabled_layers.clone(), selected_layer, }, )); } + + pub fn paint_overlay( + &mut self, + graphics_state: Arc, + painter: &egui::Painter, + rect: egui::Rect, + ) { + painter.add(egui_wgpu::Callback::new_paint_callback( + rect, + OverlayCallback { + resources: self.resources.clone(), + graphics_state, + + fog_enabled: self.fog_enabled, + coll_enabled: self.coll_enabled, + }, + )); + } } diff --git a/crates/graphics/src/tiles/atlas.rs b/crates/graphics/src/tiles/atlas.rs index c604da6d..367b9df1 100644 --- a/crates/graphics/src/tiles/atlas.rs +++ b/crates/graphics/src/tiles/atlas.rs @@ -279,7 +279,11 @@ impl Atlas { let is_under_autotiles = !is_autotile && tile_u32 - TOTAL_AUTOTILE_ID_AMOUNT < max_tiles_under_autotiles; - let atlas_tile_position = if tile_u32 < AUTOTILE_ID_AMOUNT { + let atlas_tile_position = if tile_u32 < AUTOTILE_ID_AMOUNT + || tile_u32 + >= (MAX_SIZE / TILESET_WIDTH) * ROWS_UNDER_AUTOTILES_TIMES_COLUMNS + + TOTAL_AUTOTILE_ID_AMOUNT + { egui::pos2(0., 0.) } else if is_autotile { egui::pos2( diff --git a/crates/graphics/src/tiles/tilemap.wgsl b/crates/graphics/src/tiles/tilemap.wgsl index e00b12a9..fe5cc240 100644 --- a/crates/graphics/src/tiles/tilemap.wgsl +++ b/crates/graphics/src/tiles/tilemap.wgsl @@ -52,7 +52,7 @@ fn vs_main(vertex: VertexInput, instance: InstanceInput) -> VertexOutput { let autotiles = push_constants.autotiles; #endif - if instance.tile_id < 48 { + if instance.tile_id < 48 || instance.tile_id >= (8192 / 256) * 1712 + 384 { return out; } diff --git a/crates/graphics/src/viewport.rs b/crates/graphics/src/viewport.rs index e70a0156..b514af2e 100644 --- a/crates/graphics/src/viewport.rs +++ b/crates/graphics/src/viewport.rs @@ -88,9 +88,13 @@ impl Viewport { } } - pub fn bind<'rpass>(&'rpass self, render_pass: &mut wgpu::RenderPass<'rpass>) { + pub fn bind<'rpass>( + &'rpass self, + group_index: u32, + render_pass: &mut wgpu::RenderPass<'rpass>, + ) { if let Some(uniform) = &self.uniform { - render_pass.set_bind_group(1, &uniform.bind_group, &[]); + render_pass.set_bind_group(group_index, &uniform.bind_group, &[]); } } } diff --git a/crates/ui/src/tabs/map/mod.rs b/crates/ui/src/tabs/map/mod.rs index 2755cd67..e8a2034e 100644 --- a/crates/ui/src/tabs/map/mod.rs +++ b/crates/ui/src/tabs/map/mod.rs @@ -81,6 +81,10 @@ pub struct Tab { tilemap_undo_cache: Vec, /// The layer tilemap_undo_cache refers to tilemap_undo_cache_layer: usize, + + /// This stores the passage values for every position on the map so that we can figure out + /// which passage values have changed in the current frame + passages: luminol_data::Table2, } // TODO: If we add support for changing event IDs, these need to be added as history entries @@ -116,6 +120,18 @@ impl Tab { let map = update_state .data .get_or_load_map(id, update_state.filesystem); + let tilesets = update_state.data.tilesets(); + let tileset = &tilesets[map.tileset_id]; + + let mut passages = luminol_data::Table2::new(map.data.xsize(), map.data.ysize()); + luminol_graphics::collision::calculate_passages( + &tileset.passages, + &tileset.priorities, + &map.data, + Some(&map.events), + (0..map.data.zsize()).rev(), + |x, y, passage| passages[(x, y)] = passage, + ); Ok(Self { id, @@ -139,6 +155,8 @@ impl Tab { redo_history: Vec::with_capacity(HISTORY_SIZE), tilemap_undo_cache: vec![0; map.data.xsize() * map.data.ysize()], tilemap_undo_cache_layer: 0, + + passages, }) } } @@ -186,33 +204,46 @@ impl luminol_core::Tab for Tab { |ui| { // TODO: Add layer enable button // Display all layers. - ui.columns(2, |columns| { - columns[1].visuals_mut().button_frame = true; - columns[0].label(egui::RichText::new("Panorama").underline()); - columns[1].checkbox(&mut self.view.map.pano_enabled, "👁"); - - for (index, layer) in - self.view.map.enabled_layers.iter_mut().enumerate() - { - columns[0].selectable_value( - &mut self.view.selected_layer, - luminol_components::SelectedLayer::Tiles(index), - format!("Layer {}", index + 1), - ); - columns[1].checkbox(layer, "👁"); - } - - // Display event layer. - columns[0].selectable_value( - &mut self.view.selected_layer, - luminol_components::SelectedLayer::Events, - egui::RichText::new("Events").italics(), - ); - columns[1].checkbox(&mut self.view.event_enabled, "👁"); + egui::Grid::new(self.id().with("layer_select")) + .striped(true) + .show(ui, |ui| { + ui.label(egui::RichText::new("Panorama").underline()); + ui.checkbox(&mut self.view.map.pano_enabled, "👁"); + ui.end_row(); + + for (index, layer) in + self.view.map.enabled_layers.iter_mut().enumerate() + { + ui.columns(1, |columns| { + columns[0].selectable_value( + &mut self.view.selected_layer, + luminol_components::SelectedLayer::Tiles(index), + format!("Layer {}", index + 1), + ); + }); + ui.checkbox(layer, "👁"); + ui.end_row(); + } - columns[0].label(egui::RichText::new("Fog").underline()); - columns[1].checkbox(&mut self.view.map.fog_enabled, "👁"); - }); + // Display event layer. + ui.columns(1, |columns| { + columns[0].selectable_value( + &mut self.view.selected_layer, + luminol_components::SelectedLayer::Events, + egui::RichText::new("Events").italics(), + ); + }); + ui.checkbox(&mut self.view.event_enabled, "👁"); + ui.end_row(); + + ui.label(egui::RichText::new("Fog").underline()); + ui.checkbox(&mut self.view.map.fog_enabled, "👁"); + ui.end_row(); + + ui.label(egui::RichText::new("Collision").underline()); + ui.checkbox(&mut self.view.map.coll_enabled, "👁"); + ui.end_row(); + }); }, ); @@ -256,7 +287,8 @@ impl luminol_core::Tab for Tab { .max_width(tilepicker_default_width) .show_inside(ui, |ui| { egui::ScrollArea::both().show_viewport(ui, |ui, rect| { - self.tilepicker.ui(update_state, ui, rect); + self.tilepicker + .ui(update_state, ui, rect, self.view.map.coll_enabled); ui.separator(); }); }); @@ -265,6 +297,8 @@ impl luminol_core::Tab for Tab { egui::Frame::canvas(ui.style()).show(ui, |ui| { // Get the map. let mut map = update_state.data.get_map(self.id); + let tilesets = update_state.data.tilesets(); + let tileset = &tilesets[map.tileset_id]; // Save the state of the selected layer into the cache if let luminol_components::SelectedLayer::Tiles(tile_layer) = @@ -562,6 +596,31 @@ impl luminol_core::Tab for Tab { } } } + + // Update the collision preview + luminol_graphics::collision::calculate_passages( + &tileset.passages, + &tileset.priorities, + &map.data, + if self.view.event_enabled { + Some(&map.events) + } else { + None + }, + (0..map.data.zsize()) + .filter(|&i| self.view.map.enabled_layers[i]) + .rev(), + |x, y, passage| { + if self.passages[(x, y)] != passage { + self.view.map.set_passage( + &update_state.graphics.render_state, + passage, + (x, y), + ); + self.passages[(x, y)] = passage; + } + }, + ); }) });