From 8506d8448bde9ec4efae8bcc6d2e6ff392a5e87c Mon Sep 17 00:00:00 2001 From: kokosha Date: Tue, 19 Nov 2024 19:27:02 -0300 Subject: [PATCH 1/3] Remove the white border in entity --- korangar/src/graphics/instruction.rs | 1 + .../src/graphics/passes/forward/entity.rs | 5 +- .../passes/forward/shader/entity.wgsl | 6 +- .../forward/shader/entity_bindless.wgsl | 6 +- korangar/src/loaders/sprite/mod.rs | 155 ++++++++++++++---- korangar/src/world/animation/mod.rs | 2 + 6 files changed, 142 insertions(+), 33 deletions(-) diff --git a/korangar/src/graphics/instruction.rs b/korangar/src/graphics/instruction.rs index 72d34170..885f7599 100644 --- a/korangar/src/graphics/instruction.rs +++ b/korangar/src/graphics/instruction.rs @@ -185,6 +185,7 @@ pub struct EntityInstruction { pub color: Color, pub mirror: bool, pub entity_id: EntityId, + pub opaque: bool, pub texture: Arc, } diff --git a/korangar/src/graphics/passes/forward/entity.rs b/korangar/src/graphics/passes/forward/entity.rs index 8a7e1fa3..66d86cf7 100644 --- a/korangar/src/graphics/passes/forward/entity.rs +++ b/korangar/src/graphics/passes/forward/entity.rs @@ -36,8 +36,9 @@ pub(crate) struct InstanceData { angle: f32, curvature: f32, mirror: u32, + opaque: u32, texture_index: i32, - padding: [u32; 2], + padding: u32, } pub(crate) struct ForwardEntityDrawer { @@ -265,6 +266,7 @@ impl Prepare for ForwardEntityDrawer { depth_offset: instruction.depth_offset, curvature: instruction.curvature, mirror: instruction.mirror as u32, + opaque: instruction.opaque as u32, texture_index, padding: Default::default(), }); @@ -289,6 +291,7 @@ impl Prepare for ForwardEntityDrawer { depth_offset: instruction.depth_offset, curvature: instruction.curvature, mirror: instruction.mirror as u32, + opaque: instruction.opaque as u32, texture_index: 0, padding: Default::default(), }); diff --git a/korangar/src/graphics/passes/forward/shader/entity.wgsl b/korangar/src/graphics/passes/forward/shader/entity.wgsl index 899df0b6..9edc6706 100644 --- a/korangar/src/graphics/passes/forward/shader/entity.wgsl +++ b/korangar/src/graphics/passes/forward/shader/entity.wgsl @@ -38,6 +38,7 @@ struct InstanceData { angle: f32, curvature: f32, mirror: u32, + opaque: u32, texture_index: i32, } @@ -63,6 +64,7 @@ struct VertexOutput { @location(6) @interpolate(flat) original_curvature: f32, @location(7) angle: f32, @location(8) color: vec4, + @location(9) opaque: u32, } struct FragmentOutput { @@ -118,6 +120,7 @@ fn vs_main( output.original_curvature = instance.curvature; output.angle = instance.angle; output.color = instance.color; + output.opaque = instance.opaque; return output; } @@ -129,13 +132,14 @@ fn fs_main(input: VertexOutput) -> FragmentOutput { let rotate = vec2(input.texture_coordinates.x - 0.5, input.texture_coordinates.y - 0.5) * mat2x2(cos_factor, sin_factor, -sin_factor, cos_factor); let texture_coordinates = vec2(clamp(rotate.x + 0.5, 0.0, 1.0), clamp(rotate.y + 0.5, 0.0, 1.0)); - let diffuse_color = textureSample(texture, texture_sampler, texture_coordinates); + var diffuse_color = textureSample(texture, texture_sampler, texture_coordinates); let alpha_channel = textureSample(texture, nearest_sampler, texture_coordinates).a; if (alpha_channel == 0.0) { discard; } + diffuse_color.a = select(diffuse_color.a, 1.0, input.opaque != 0u); // Calculate which tile this fragment belongs to let pixel_position = vec2(floor(input.position.xy)); let tile_x = pixel_position.x / TILE_SIZE; diff --git a/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl b/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl index ddac21e4..fe7c60dc 100644 --- a/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl +++ b/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl @@ -38,6 +38,7 @@ struct InstanceData { angle: f32, curvature: f32, mirror: u32, + opaque: u32, texture_index: i32, } @@ -64,6 +65,7 @@ struct VertexOutput { @location(7) texture_index: i32, @location(8) angle: f32, @location(9) color: vec4, + @location(10) opaque: u32, } struct FragmentOutput { @@ -121,6 +123,7 @@ fn vs_main( output.texture_index = instance.texture_index; output.angle = instance.angle; output.color = instance.color; + output.opaque = instance.opaque; return output; } @@ -132,13 +135,14 @@ fn fs_main(input: VertexOutput) -> FragmentOutput { let rotate = vec2(input.texture_coordinates.x - 0.5, input.texture_coordinates.y - 0.5) * mat2x2(cos_factor, sin_factor, -sin_factor, cos_factor); let texture_coordinates = vec2(clamp(rotate.x + 0.5, 0.0, 1.0), clamp(rotate.y + 0.5, 0.0, 1.0)); - let diffuse_color = textureSample(textures[input.texture_index], texture_sampler, texture_coordinates); + var diffuse_color = textureSample(textures[input.texture_index], texture_sampler, texture_coordinates); let alpha_channel = textureSample(textures[input.texture_index], nearest_sampler, texture_coordinates).a; if (alpha_channel == 0.0) { discard; } + diffuse_color.a = select(diffuse_color.a, 1.0, input.opaque != 0u); // Calculate which tile this fragment belongs to let pixel_position = vec2(floor(input.position.xy)); let tile_x = pixel_position.x / TILE_SIZE; diff --git a/korangar/src/loaders/sprite/mod.rs b/korangar/src/loaders/sprite/mod.rs index 77380ad8..a4f2324d 100644 --- a/korangar/src/loaders/sprite/mod.rs +++ b/korangar/src/loaders/sprite/mod.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; +use image::{Pixel, Rgba, RgbaImage}; #[cfg(feature = "debug")] use korangar_debug::logging::{print_debug, Colorize, Timer}; use korangar_interface::elements::PrototypeElement; @@ -24,6 +25,8 @@ pub struct Sprite { pub palette_size: usize, #[hidden_element] pub textures: Vec>, + #[hidden_element] + pub vec_opaque: Vec>, #[cfg(feature = "debug")] sprite_data: SpriteData, } @@ -89,33 +92,7 @@ impl SpriteLoader { let cloned_sprite_data = sprite_data.clone(); let palette = sprite_data.palette.unwrap(); // unwrap_or_default() as soon as i know what - - let rgba_images: Vec = sprite_data - .rgba_image_data - .iter() - .map(|image_data| { - // Revert the rows, the image is flipped upside down - // Convert the pixel from ABGR format to RGBA format - let width = image_data.width; - let data = image_data - .data - .chunks_exact(4 * width as usize) - .rev() - .flat_map(|pixels| { - pixels - .chunks_exact(4) - .flat_map(|pixel| [pixel[3], pixel[2], pixel[1], pixel[0]]) - .collect::>() - }) - .collect(); - - RgbaImageData { - width: image_data.width, - height: image_data.height, - data, - } - }) - .collect(); + let mut vec_opaque = Vec::new(); // TODO: Move this to an extension trait in `korangar_loaders`. pub fn color_bytes(palette: &PaletteColor, index: u8) -> [u8; 4] { @@ -136,17 +113,49 @@ impl SpriteLoader { .flat_map(|palette_index| color_bytes(&palette.colors[*palette_index as usize], *palette_index)) .collect(); - RgbaImageData { + let rgba_image_data = RgbaImageData { width: image_data.width, height: image_data.height, data, - } + }; + let is_opaque = SpriteLoader::is_opaque(&rgba_image_data); + (SpriteLoader::recolor_border(rgba_image_data), is_opaque) }); let palette_size = palette_images.len(); + let rgba_images: Vec<(RgbaImageData, bool)> = sprite_data + .rgba_image_data + .iter() + .map(|image_data| { + // Revert the rows, the image is flipped upside down + // Convert the pixel from ABGR format to RGBA format + let width = image_data.width; + let data = image_data + .data + .chunks_exact(4 * width as usize) + .rev() + .flat_map(|pixels| { + pixels + .chunks_exact(4) + .flat_map(|pixel| [pixel[3], pixel[2], pixel[1], pixel[0]]) + .collect::>() + }) + .collect(); + + let rgba_image_data = RgbaImageData { + width: image_data.width, + height: image_data.height, + data, + }; + let is_opaque = SpriteLoader::is_opaque(&rgba_image_data); + (SpriteLoader::recolor_border(rgba_image_data), is_opaque) + }) + .collect(); + let textures = palette_images .chain(rgba_images) - .map(|image_data| { + .map(|(image_data, is_opaque)| { + vec_opaque.push(Arc::new(is_opaque)); let texture = Texture::new_with_data( &self.device, &self.queue, @@ -175,6 +184,7 @@ impl SpriteLoader { textures, #[cfg(feature = "debug")] sprite_data: cloned_sprite_data, + vec_opaque, }); let _ = self.cache.insert(path.to_string(), sprite.clone()); @@ -190,4 +200,89 @@ impl SpriteLoader { None => self.load(path), } } + + // Recolor the transparent border with the mean of adjacent color. + fn recolor_border(rgba_image_data: RgbaImageData) -> RgbaImageData { + let rgba_image = RgbaImage::from_raw( + rgba_image_data.width as u32, + rgba_image_data.height as u32, + rgba_image_data.data, + ) + .unwrap(); + + let mut rgba_image_result = rgba_image.clone(); + + let pixel_width = rgba_image_data.width as u32; + let pixel_height = rgba_image_data.height as u32; + let width = rgba_image_data.width as i32; + let height = rgba_image_data.height as i32; + + for x in 0..pixel_width { + let x_i32 = x as i32; + for y in 0..pixel_height { + let pixel = rgba_image.get_pixel(x, y).channels(); + if pixel[3] != 0 { + continue; + } + let y_i32 = y as i32; + let mut total_r = 0; + let mut total_g = 0; + let mut total_b = 0; + let mut pixel_count = 0; + for dx in -1..=1 { + for dy in -1..=1 { + if dx == 0 && dy == 0 { + continue; + } + let nx = x_i32 + dx; + let ny = y_i32 + dy; + if 0 <= nx && nx < width && 0 <= ny && ny < height { + let close_pixel = rgba_image.get_pixel(nx as u32, ny as u32).channels(); + if close_pixel[3] != 0 { + total_r += close_pixel[0] as u32; + total_g += close_pixel[1] as u32; + total_b += close_pixel[2] as u32; + pixel_count += 1; + } + } + } + } + if pixel_count != 0 { + let mean_r = total_r / pixel_count; + let mean_g = total_g / pixel_count; + let mean_b = total_b / pixel_count; + let color = Rgba([mean_r as u8, mean_g as u8, mean_b as u8, 0]); + rgba_image_result.put_pixel(x, y, color); + } + } + } + + RgbaImageData { + width: rgba_image_data.width, + height: rgba_image_data.height, + data: rgba_image_result.into_raw(), + } + } + + fn is_opaque(rgba_image_data: &RgbaImageData) -> bool { + let rgba_image = RgbaImage::from_raw( + rgba_image_data.width as u32, + rgba_image_data.height as u32, + rgba_image_data.data.clone(), + ) + .unwrap(); + + let pixel_width = rgba_image_data.width as u32; + let pixel_height = rgba_image_data.height as u32; + + for x in 0..pixel_width { + for y in 0..pixel_height { + let pixel = rgba_image.get_pixel(x, y).channels(); + if pixel[3] != 0 && pixel[3] != 255 { + return false; + } + } + } + return true; + } } diff --git a/korangar/src/world/animation/mod.rs b/korangar/src/world/animation/mod.rs index 77d933d6..ee26002c 100644 --- a/korangar/src/world/animation/mod.rs +++ b/korangar/src/world/animation/mod.rs @@ -113,6 +113,7 @@ impl AnimationData { let animation_index = frame_part.animation_index; let sprite_number = frame_part.sprite_number; let texture = &self.animation_pair[animation_index].sprites.textures[sprite_number]; + let opaque = &self.animation_pair[animation_index].sprites.vec_opaque[sprite_number]; // The constant 10.0 is a magic scale factor of an image. // The vertex position is calculated from the center of image, so we need to @@ -145,6 +146,7 @@ impl AnimationData { angle: frame_part.angle, color: frame_part.color, mirror: frame_part.mirror, + opaque: **opaque && (frame_part.color.alpha == 1.0), entity_id, texture: texture.clone(), }); From 66ff7c12752c4f78534f3c5d54fd3de1055a706a Mon Sep 17 00:00:00 2001 From: kokosha Date: Fri, 22 Nov 2024 10:00:34 -0300 Subject: [PATCH 2/3] Add sorting and fix the picker --- korangar/src/graphics/instruction.rs | 1 + .../src/graphics/passes/forward/shader/entity.wgsl | 3 +++ .../passes/forward/shader/entity_bindless.wgsl | 3 +++ korangar/src/graphics/passes/picker/entity.rs | 5 +++-- korangar/src/main.rs | 11 +++++++++++ korangar/src/world/animation/mod.rs | 2 ++ korangar/src/world/entity/mod.rs | 7 ++++--- korangar/src/world/map/mod.rs | 9 +++++---- 8 files changed, 32 insertions(+), 9 deletions(-) diff --git a/korangar/src/graphics/instruction.rs b/korangar/src/graphics/instruction.rs index 885f7599..bada0d6d 100644 --- a/korangar/src/graphics/instruction.rs +++ b/korangar/src/graphics/instruction.rs @@ -186,6 +186,7 @@ pub struct EntityInstruction { pub mirror: bool, pub entity_id: EntityId, pub opaque: bool, + pub pickerable: bool, pub texture: Arc, } diff --git a/korangar/src/graphics/passes/forward/shader/entity.wgsl b/korangar/src/graphics/passes/forward/shader/entity.wgsl index 9edc6706..bb001276 100644 --- a/korangar/src/graphics/passes/forward/shader/entity.wgsl +++ b/korangar/src/graphics/passes/forward/shader/entity.wgsl @@ -139,7 +139,10 @@ fn fs_main(input: VertexOutput) -> FragmentOutput { discard; } + // TODO - OPAQUE: The correct way is to have a pass without blending for opaque entity framepart + // and a pass with blending for semi-transparent entity framepart diffuse_color.a = select(diffuse_color.a, 1.0, input.opaque != 0u); + // Calculate which tile this fragment belongs to let pixel_position = vec2(floor(input.position.xy)); let tile_x = pixel_position.x / TILE_SIZE; diff --git a/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl b/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl index fe7c60dc..24972235 100644 --- a/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl +++ b/korangar/src/graphics/passes/forward/shader/entity_bindless.wgsl @@ -142,7 +142,10 @@ fn fs_main(input: VertexOutput) -> FragmentOutput { discard; } + // TODO - OPAQUE: The correct way is to have a pass without blending for opaque entity framepart + // and a pass with blending for semi-transparent entity framepart. diffuse_color.a = select(diffuse_color.a, 1.0, input.opaque != 0u); + // Calculate which tile this fragment belongs to let pixel_position = vec2(floor(input.position.xy)); let tile_x = pixel_position.x / TILE_SIZE; diff --git a/korangar/src/graphics/passes/picker/entity.rs b/korangar/src/graphics/passes/picker/entity.rs index f69f1ca4..2be34db8 100644 --- a/korangar/src/graphics/passes/picker/entity.rs +++ b/korangar/src/graphics/passes/picker/entity.rs @@ -214,6 +214,7 @@ impl Drawer<{ BindGroupCount::One }, { ColorAttachmentCount::One }, { DepthAttac impl Prepare for PickerEntityDrawer { fn prepare(&mut self, device: &Device, instructions: &RenderInstruction) { + // TODO: Pass the player size directly let player_size = 2; self.draw_count = instructions.entities.len().saturating_sub(player_size); @@ -232,7 +233,7 @@ impl Prepare for PickerEntityDrawer { // We skip the first entity, because we don't want the player entity to show up // in the picker buffer. // TODO: Remove the player entity correctly. - for instruction in instructions.entities.iter().skip(player_size) { + for instruction in instructions.entities.iter().filter(|instruction| instruction.pickerable == true) { let picker_target = PickerTarget::Entity(instruction.entity_id); let (identifier_high, identifier_low) = picker_target.into(); @@ -267,7 +268,7 @@ impl Prepare for PickerEntityDrawer { // We skip the first entity, because we don't want the player entity to show up // in the picker buffer. // TODO: Remove the player entity correctly. - for instruction in instructions.entities.iter().skip(player_size) { + for instruction in instructions.entities.iter().filter(|instruction| instruction.pickerable == true) { let picker_target = PickerTarget::Entity(instruction.entity_id); let (identifier_high, identifier_low) = picker_target.into(); diff --git a/korangar/src/main.rs b/korangar/src/main.rs index 334988bd..90625535 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -2035,6 +2035,17 @@ impl Client { #[cfg_attr(feature = "debug", korangar_debug::debug_condition(render_settings.show_entities))] self.map .render_entities(&mut self.entity_instructions, entities, current_camera, true); + // TODO - OPAQUE: Separate entity_instruction into opaque and semi-transparent. + // For opaque pipeline, disable alpha blending; for semi-transparent pipeline, + // enable alpha blending. + self.entity_instructions.sort_by(|a, b| { + if a.opaque == b.opaque { + // As we use reverse depth offset, the comparisor is inverted + a.depth_offset.partial_cmp(&b.depth_offset).unwrap() + } else { + b.opaque.cmp(&a.opaque) + } + }); #[cfg_attr(feature = "debug", korangar_debug::debug_condition(render_settings.show_water))] self.map.render_water(&mut map_water_vertex_buffer); diff --git a/korangar/src/world/animation/mod.rs b/korangar/src/world/animation/mod.rs index ee26002c..3da20a31 100644 --- a/korangar/src/world/animation/mod.rs +++ b/korangar/src/world/animation/mod.rs @@ -82,6 +82,7 @@ impl AnimationData { entity_position: Point3, animation_state: &AnimationState, head_direction: usize, + pickerable: bool, ) { let camera_direction = camera.camera_direction(); let direction = (camera_direction + head_direction) % 8; @@ -147,6 +148,7 @@ impl AnimationData { color: frame_part.color, mirror: frame_part.mirror, opaque: **opaque && (frame_part.color.alpha == 1.0), + pickerable, entity_id, texture: texture.clone(), }); diff --git a/korangar/src/world/entity/mod.rs b/korangar/src/world/entity/mod.rs index 8e528903..31820705 100644 --- a/korangar/src/world/entity/mod.rs +++ b/korangar/src/world/entity/mod.rs @@ -762,7 +762,7 @@ impl Common { } } - pub fn render(&self, instructions: &mut Vec, camera: &dyn Camera) { + pub fn render(&self, instructions: &mut Vec, camera: &dyn Camera, pickerable: bool) { self.animation_data.render( instructions, camera, @@ -770,6 +770,7 @@ impl Common { self.position, &self.animation_state, self.head_direction, + pickerable, ); } @@ -1130,8 +1131,8 @@ impl Entity { .generate_pathing_mesh(device, queue, map, pathing_texture_mapping); } - pub fn render(&self, instructions: &mut Vec, camera: &dyn Camera) { - self.get_common().render(instructions, camera); + pub fn render(&self, instructions: &mut Vec, camera: &dyn Camera, pickerable: bool) { + self.get_common().render(instructions, camera, pickerable); } #[cfg(feature = "debug")] diff --git a/korangar/src/world/map/mod.rs b/korangar/src/world/map/mod.rs index 1ab35ae6..98848ef4 100644 --- a/korangar/src/world/map/mod.rs +++ b/korangar/src/world/map/mod.rs @@ -265,10 +265,11 @@ impl Map { #[cfg_attr(feature = "debug", korangar_debug::profile)] pub fn render_entities(&self, instructions: &mut Vec, entities: &[Entity], camera: &dyn Camera, include_self: bool) { - entities - .iter() - .skip(!include_self as usize) - .for_each(|entity| entity.render(instructions, camera)); + let mut is_current_player = true || include_self; + entities.iter().skip(!include_self as usize).for_each(|entity| { + entity.render(instructions, camera, is_current_player); + is_current_player = false; + }); } #[cfg(feature = "debug")] From 60f7bef710bfb602403c93912e4f2d065eeb3dee Mon Sep 17 00:00:00 2001 From: kokosha Date: Sun, 24 Nov 2024 15:41:11 -0300 Subject: [PATCH 3/3] Small fix --- korangar/src/world/map/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/korangar/src/world/map/mod.rs b/korangar/src/world/map/mod.rs index 98848ef4..fa819014 100644 --- a/korangar/src/world/map/mod.rs +++ b/korangar/src/world/map/mod.rs @@ -267,7 +267,7 @@ impl Map { pub fn render_entities(&self, instructions: &mut Vec, entities: &[Entity], camera: &dyn Camera, include_self: bool) { let mut is_current_player = true || include_self; entities.iter().skip(!include_self as usize).for_each(|entity| { - entity.render(instructions, camera, is_current_player); + entity.render(instructions, camera, !is_current_player); is_current_player = false; }); }