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(), });