Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the white border in entity #149

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions korangar/src/graphics/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ pub struct EntityInstruction {
pub color: Color,
pub mirror: bool,
pub entity_id: EntityId,
pub opaque: bool,
pub pickerable: bool,
pub texture: Arc<Texture>,
}

Expand Down
5 changes: 4 additions & 1 deletion korangar/src/graphics/passes/forward/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
});
Expand All @@ -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(),
});
Expand Down
9 changes: 8 additions & 1 deletion korangar/src/graphics/passes/forward/shader/entity.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ struct InstanceData {
angle: f32,
curvature: f32,
mirror: u32,
opaque: u32,
texture_index: i32,
}

Expand All @@ -63,6 +64,7 @@ struct VertexOutput {
@location(6) @interpolate(flat) original_curvature: f32,
@location(7) angle: f32,
@location(8) color: vec4<f32>,
@location(9) opaque: u32,
}

struct FragmentOutput {
Expand Down Expand Up @@ -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;
}

Expand All @@ -129,13 +132,17 @@ 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;
}

// 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<u32>(floor(input.position.xy));
let tile_x = pixel_position.x / TILE_SIZE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ struct InstanceData {
angle: f32,
curvature: f32,
mirror: u32,
opaque: u32,
texture_index: i32,
}

Expand All @@ -64,6 +65,7 @@ struct VertexOutput {
@location(7) texture_index: i32,
@location(8) angle: f32,
@location(9) color: vec4<f32>,
@location(10) opaque: u32,
}

struct FragmentOutput {
Expand Down Expand Up @@ -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;
}

Expand All @@ -132,13 +135,17 @@ 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;
}

// 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<u32>(floor(input.position.xy));
let tile_x = pixel_position.x / TILE_SIZE;
Expand Down
5 changes: 3 additions & 2 deletions korangar/src/graphics/passes/picker/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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();

Expand Down Expand Up @@ -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();

Expand Down
155 changes: 125 additions & 30 deletions korangar/src/loaders/sprite/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,6 +25,8 @@ pub struct Sprite {
pub palette_size: usize,
#[hidden_element]
pub textures: Vec<Arc<Texture>>,
#[hidden_element]
pub vec_opaque: Vec<Arc<bool>>,
#[cfg(feature = "debug")]
sprite_data: SpriteData,
}
Expand Down Expand Up @@ -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<RgbaImageData> = 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::<Vec<u8>>()
})
.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] {
Expand All @@ -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::<Vec<u8>>()
})
.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,
Expand Down Expand Up @@ -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());

Expand All @@ -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;
}
}
11 changes: 11 additions & 0 deletions korangar/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions korangar/src/world/animation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ impl AnimationData {
entity_position: Point3<f32>,
animation_state: &AnimationState,
head_direction: usize,
pickerable: bool,
) {
let camera_direction = camera.camera_direction();
let direction = (camera_direction + head_direction) % 8;
Expand Down Expand Up @@ -113,6 +114,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
Expand Down Expand Up @@ -145,6 +147,8 @@ impl AnimationData {
angle: frame_part.angle,
color: frame_part.color,
mirror: frame_part.mirror,
opaque: **opaque && (frame_part.color.alpha == 1.0),
pickerable,
entity_id,
texture: texture.clone(),
});
Expand Down
Loading