From c5dd17a5d9628a80f73fae6af229b0f619a77858 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 09:38:25 +0000 Subject: [PATCH 01/28] Add optional transparancy passthrough for sprite backend --- crates/bevy_sprite/src/picking_backend.rs | 61 +++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index ad2afc33d1954..65c07e51e6803 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -11,16 +11,39 @@ use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_math::{prelude::*, FloatExt, FloatOrd}; use bevy_picking::backend::prelude::*; +use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; +/// Runtime settings for the [`SpriteBackend`]. +#[derive(Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct SpriteBackendSettings { + /// When set to `true` picking will ignore any part of a sprite which is transparent + /// Off by default for backwards compatibility. This setting is provided to give you fine-grained + /// control over if transparncy on sprites is ignored. + pub passthrough_transparency: bool, + /// How Opaque does part of a sprite need to be in order count as none-transparent (defaults to 10) + pub transparency_cutoff: u8, +} + +impl Default for SpriteBackendSettings { + fn default() -> Self { + Self { + passthrough_transparency: true, + transparency_cutoff: 10, + } + } +} + #[derive(Clone)] pub struct SpritePickingPlugin; impl Plugin for SpritePickingPlugin { fn build(&self, app: &mut App) { - app.add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend)); + app.init_resource::() + .add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend)); } } @@ -30,6 +53,7 @@ pub fn sprite_picking( primary_window: Query>, images: Res>, texture_atlas_layout: Res>, + settings: Res, sprite_query: Query<( Entity, &Sprite, @@ -130,12 +154,43 @@ pub fn sprite_picking( let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); - blocked = is_cursor_in_sprite + let cursor_in_valid_pixels_of_sprite = is_cursor_in_sprite + && (!settings.passthrough_transparency || { + let texture: &Image = images.get(&sprite.image)?; + // If using a texture atlas, grab the offset of the current sprite index. (0,0) otherwise + let texture_rect = sprite + .texture_atlas + .as_ref() + .and_then(|atlas| { + texture_atlas_layout + .get(&atlas.layout) + .map(|f| f.textures[atlas.index]) + }) + .or(Some(URect::new(0, 0, texture.width(), texture.height())))?; + // get mouse position on texture + let texture_position = (texture_rect.center().as_vec2() + + cursor_pos_sprite.trunc()) + .as_uvec2(); + // grab pixel + let pixel_index = + (texture_position.y * texture.width() + texture_position.x) as usize; + // check transparancy + if let Some(pixel_data) = + texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) + { + let transparency = pixel_data[3]; + transparency > settings.transparency_cutoff + } else { + false + } + }); + + blocked = cursor_in_valid_pixels_of_sprite && picking_behavior .map(|p| p.should_block_lower) .unwrap_or(true); - is_cursor_in_sprite.then(|| { + cursor_in_valid_pixels_of_sprite.then(|| { let hit_pos_world = sprite_transform.transform_point(cursor_pos_sprite.extend(0.0)); // Transform point from world to camera space to get the Z distance From bba576b67ff04d5549a06db68ad8d8eb5b05eded Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 09:39:49 +0000 Subject: [PATCH 02/28] set correct default --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 65c07e51e6803..b59917c7a902e 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -31,7 +31,7 @@ pub struct SpriteBackendSettings { impl Default for SpriteBackendSettings { fn default() -> Self { Self { - passthrough_transparency: true, + passthrough_transparency: false, transparency_cutoff: 10, } } From f9088c793af70d70d69437f6d893100304f6a4b7 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 09:44:44 +0000 Subject: [PATCH 03/28] fix typo --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index b59917c7a902e..cfe29c823248a 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -174,7 +174,7 @@ pub fn sprite_picking( // grab pixel let pixel_index = (texture_position.y * texture.width() + texture_position.x) as usize; - // check transparancy + // check transparency if let Some(pixel_data) = texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) { From d1013cfe85a21fbd35ae86ed8a1a48da811543d7 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 09:51:08 +0000 Subject: [PATCH 04/28] fix too many arguments --- crates/bevy_sprite/src/picking_backend.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index cfe29c823248a..939afbc36b4ea 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -47,6 +47,7 @@ impl Plugin for SpritePickingPlugin { } } +#[allow(clippy::too_many_arguments)] pub fn sprite_picking( pointers: Query<(&PointerId, &PointerLocation)>, cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>, From 1924e0256e8f9f3273c6c094cfb170dbe29771a2 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 09:58:25 +0000 Subject: [PATCH 05/28] fix doc typo --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 939afbc36b4ea..a02323fcf29eb 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -22,7 +22,7 @@ use bevy_window::PrimaryWindow; pub struct SpriteBackendSettings { /// When set to `true` picking will ignore any part of a sprite which is transparent /// Off by default for backwards compatibility. This setting is provided to give you fine-grained - /// control over if transparncy on sprites is ignored. + /// control over if transparency on sprites is ignored. pub passthrough_transparency: bool, /// How Opaque does part of a sprite need to be in order count as none-transparent (defaults to 10) pub transparency_cutoff: u8, From 42c156bd05a6c25c1276dcf5e6fa82b10a443615 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 10:02:45 +0000 Subject: [PATCH 06/28] clean up final check into match --- crates/bevy_sprite/src/picking_backend.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index a02323fcf29eb..377690f2fda7a 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -176,13 +176,13 @@ pub fn sprite_picking( let pixel_index = (texture_position.y * texture.width() + texture_position.x) as usize; // check transparency - if let Some(pixel_data) = - texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) - { - let transparency = pixel_data[3]; - transparency > settings.transparency_cutoff - } else { - false + match texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) { + // If possible check the transparency bit is above cutoff + Some(pixel_data) if pixel_data[3] > settings.transparency_cutoff => { + true + } + // If not possible, it's not in the sprite + _ => false, } }); From 3ca1039930269554364775cde298f7056d0c5a28 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 10:18:01 +0000 Subject: [PATCH 07/28] fix docs and naming --- crates/bevy_sprite/src/picking_backend.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 377690f2fda7a..d3a8b295b56e6 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -16,22 +16,24 @@ use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; -/// Runtime settings for the [`SpriteBackend`]. +/// Runtime settings for the [`SpritePickingPlugin`]. #[derive(Resource, Reflect)] #[reflect(Resource, Default)] pub struct SpriteBackendSettings { /// When set to `true` picking will ignore any part of a sprite which is transparent /// Off by default for backwards compatibility. This setting is provided to give you fine-grained /// control over if transparency on sprites is ignored. - pub passthrough_transparency: bool, + pub transparency_passthrough: bool, /// How Opaque does part of a sprite need to be in order count as none-transparent (defaults to 10) + /// + /// This is on a scale from 0 - 255 representing the alpha channel value you'd get in most art programs. pub transparency_cutoff: u8, } impl Default for SpriteBackendSettings { fn default() -> Self { Self { - passthrough_transparency: false, + transparency_passthrough: false, transparency_cutoff: 10, } } @@ -156,7 +158,7 @@ pub fn sprite_picking( let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); let cursor_in_valid_pixels_of_sprite = is_cursor_in_sprite - && (!settings.passthrough_transparency || { + && (!settings.transparency_passthrough || { let texture: &Image = images.get(&sprite.image)?; // If using a texture atlas, grab the offset of the current sprite index. (0,0) otherwise let texture_rect = sprite From 703810190a894dbec792b47777ca000b067c7115 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 14 Nov 2024 10:19:44 +0000 Subject: [PATCH 08/28] change transparency to alpha in variable names --- crates/bevy_sprite/src/picking_backend.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index d3a8b295b56e6..0cc4b9a6f076e 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -20,21 +20,21 @@ use bevy_window::PrimaryWindow; #[derive(Resource, Reflect)] #[reflect(Resource, Default)] pub struct SpriteBackendSettings { - /// When set to `true` picking will ignore any part of a sprite which is transparent + /// When set to `true` picking will ignore any part of a sprite which has an alpha lower than the cutoff /// Off by default for backwards compatibility. This setting is provided to give you fine-grained /// control over if transparency on sprites is ignored. - pub transparency_passthrough: bool, + pub alpha_passthrough: bool, /// How Opaque does part of a sprite need to be in order count as none-transparent (defaults to 10) /// /// This is on a scale from 0 - 255 representing the alpha channel value you'd get in most art programs. - pub transparency_cutoff: u8, + pub alpha_cutoff: u8, } impl Default for SpriteBackendSettings { fn default() -> Self { Self { - transparency_passthrough: false, - transparency_cutoff: 10, + alpha_passthrough: false, + alpha_cutoff: 10, } } } @@ -158,7 +158,7 @@ pub fn sprite_picking( let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); let cursor_in_valid_pixels_of_sprite = is_cursor_in_sprite - && (!settings.transparency_passthrough || { + && (!settings.alpha_passthrough || { let texture: &Image = images.get(&sprite.image)?; // If using a texture atlas, grab the offset of the current sprite index. (0,0) otherwise let texture_rect = sprite @@ -179,10 +179,8 @@ pub fn sprite_picking( (texture_position.y * texture.width() + texture_position.x) as usize; // check transparency match texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) { - // If possible check the transparency bit is above cutoff - Some(pixel_data) if pixel_data[3] > settings.transparency_cutoff => { - true - } + // If possible check the alpha bit is above cutoff + Some(pixel_data) if pixel_data[3] > settings.alpha_cutoff => true, // If not possible, it's not in the sprite _ => false, } From 1f48ff20c1cd44a0c86bca36ab4ce7a5f508ca6a Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Sun, 17 Nov 2024 09:10:00 +0000 Subject: [PATCH 09/28] Add AlphaPassthrough enum and change default to on --- crates/bevy_sprite/src/picking_backend.rs | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 0cc4b9a6f076e..4c79885b9ef0a 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -20,21 +20,23 @@ use bevy_window::PrimaryWindow; #[derive(Resource, Reflect)] #[reflect(Resource, Default)] pub struct SpriteBackendSettings { - /// When set to `true` picking will ignore any part of a sprite which has an alpha lower than the cutoff - /// Off by default for backwards compatibility. This setting is provided to give you fine-grained - /// control over if transparency on sprites is ignored. - pub alpha_passthrough: bool, - /// How Opaque does part of a sprite need to be in order count as none-transparent (defaults to 10) - /// - /// This is on a scale from 0 - 255 representing the alpha channel value you'd get in most art programs. - pub alpha_cutoff: u8, + /// Should the backend count transparent pixels as part of the sprite for picking purposes (defaults to AlphaThreshold(10)) + pub alpha_passthrough: SpriteBackendAlphaPassthrough, +} + +#[derive(Debug, Clone, Copy, Reflect)] +pub enum SpriteBackendAlphaPassthrough { + /// Even if a sprite is picked on a transparent pixel, it should still count within the backend + NoPassthrough, + /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) + /// Threshold is given as a single u8 value (0-255) representing the alpha channel that you would see in most art programs + Threshold(u8), } impl Default for SpriteBackendSettings { fn default() -> Self { Self { - alpha_passthrough: false, - alpha_cutoff: 10, + alpha_passthrough: SpriteBackendAlphaPassthrough::Threshold(10), } } } @@ -157,8 +159,8 @@ pub fn sprite_picking( let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); - let cursor_in_valid_pixels_of_sprite = is_cursor_in_sprite - && (!settings.alpha_passthrough || { + let cursor_in_valid_pixels_of_sprite = match settings.alpha_passthrough { + SpriteBackendAlphaPassthrough::Threshold(cutoff) if is_cursor_in_sprite => { let texture: &Image = images.get(&sprite.image)?; // If using a texture atlas, grab the offset of the current sprite index. (0,0) otherwise let texture_rect = sprite @@ -180,11 +182,14 @@ pub fn sprite_picking( // check transparency match texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) { // If possible check the alpha bit is above cutoff - Some(pixel_data) if pixel_data[3] > settings.alpha_cutoff => true, + Some(pixel_data) if pixel_data[3] > cutoff => true, // If not possible, it's not in the sprite _ => false, } - }); + } + SpriteBackendAlphaPassthrough::NoPassthrough => is_cursor_in_sprite, + _ => false, + }; blocked = cursor_in_valid_pixels_of_sprite && picking_behavior From 5ee14485a88579638d9656ad6498db8931613191 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Sun, 17 Nov 2024 09:11:17 +0000 Subject: [PATCH 10/28] improve comments --- crates/bevy_sprite/src/picking_backend.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 4c79885b9ef0a..3e463e862179d 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -26,7 +26,8 @@ pub struct SpriteBackendSettings { #[derive(Debug, Clone, Copy, Reflect)] pub enum SpriteBackendAlphaPassthrough { - /// Even if a sprite is picked on a transparent pixel, it should still count within the backend + /// Even if a sprite is picked on a transparent pixel, it should still count within the backend. + /// Only consider the rect of a given sprite. NoPassthrough, /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) /// Threshold is given as a single u8 value (0-255) representing the alpha channel that you would see in most art programs From 38eb82352e7b9025a01f82a1bdbc30c9d6185544 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Sun, 17 Nov 2024 09:16:26 +0000 Subject: [PATCH 11/28] update comments --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 124a5eadd553d..7c92cb4bd5b9c 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -20,7 +20,7 @@ use bevy_window::PrimaryWindow; #[derive(Resource, Reflect)] #[reflect(Resource, Default)] pub struct SpriteBackendSettings { - /// Should the backend count transparent pixels as part of the sprite for picking purposes (defaults to AlphaThreshold(10)) + /// Should the backend count transparent pixels as part of the sprite for picking purposes (defaults to Threshold(10)) pub alpha_passthrough: SpriteBackendAlphaPassthrough, } From 26f4511c43ae9a9a38334e44648ef00c69a4b535 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Sun, 17 Nov 2024 09:18:36 +0000 Subject: [PATCH 12/28] update comments --- crates/bevy_sprite/src/picking_backend.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 7c92cb4bd5b9c..407a01fe77446 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -16,14 +16,7 @@ use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; -/// Runtime settings for the [`SpritePickingPlugin`]. -#[derive(Resource, Reflect)] -#[reflect(Resource, Default)] -pub struct SpriteBackendSettings { - /// Should the backend count transparent pixels as part of the sprite for picking purposes (defaults to Threshold(10)) - pub alpha_passthrough: SpriteBackendAlphaPassthrough, -} - +/// Should the backend count transparent pixels as part of the sprite for picking purposes #[derive(Debug, Clone, Copy, Reflect)] pub enum SpriteBackendAlphaPassthrough { /// Even if a sprite is picked on a transparent pixel, it should still count within the backend. @@ -34,6 +27,14 @@ pub enum SpriteBackendAlphaPassthrough { Threshold(u8), } +/// Runtime settings for the [`SpritePickingPlugin`]. +#[derive(Resource, Reflect)] +#[reflect(Resource, Default)] +pub struct SpriteBackendSettings { + /// Should the backend count transparent pixels as part of the sprite for picking purposes (defaults to Threshold(10)) + pub alpha_passthrough: SpriteBackendAlphaPassthrough, +} + impl Default for SpriteBackendSettings { fn default() -> Self { Self { From 07376313bc0ab83ec239c35cf555b4101f19fe54 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Mon, 18 Nov 2024 09:50:46 +0000 Subject: [PATCH 13/28] rename Sprite Picking Mode --- crates/bevy_sprite/src/picking_backend.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 407a01fe77446..7af965ef70109 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -16,29 +16,31 @@ use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; -/// Should the backend count transparent pixels as part of the sprite for picking purposes +/// How should the [`SpritePickingPlugin`] handle picking with tranparent pixels #[derive(Debug, Clone, Copy, Reflect)] -pub enum SpriteBackendAlphaPassthrough { +pub enum SpritePickingMode { /// Even if a sprite is picked on a transparent pixel, it should still count within the backend. /// Only consider the rect of a given sprite. - NoPassthrough, + BoundingBox, /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) /// Threshold is given as a single u8 value (0-255) representing the alpha channel that you would see in most art programs - Threshold(u8), + AlphaThreshold(u8), } /// Runtime settings for the [`SpritePickingPlugin`]. #[derive(Resource, Reflect)] #[reflect(Resource, Default)] pub struct SpriteBackendSettings { - /// Should the backend count transparent pixels as part of the sprite for picking purposes (defaults to Threshold(10)) - pub alpha_passthrough: SpriteBackendAlphaPassthrough, + /// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone. + /// + /// Defaults to an incusive alpha threshold of 10/255 + pub picking_mode: SpritePickingMode, } impl Default for SpriteBackendSettings { fn default() -> Self { Self { - alpha_passthrough: SpriteBackendAlphaPassthrough::Threshold(10), + picking_mode: SpritePickingMode::AlphaThreshold(10), } } } @@ -166,8 +168,8 @@ pub fn sprite_picking( let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); - let cursor_in_valid_pixels_of_sprite = match settings.alpha_passthrough { - SpriteBackendAlphaPassthrough::Threshold(cutoff) if is_cursor_in_sprite => { + let cursor_in_valid_pixels_of_sprite = match settings.picking_mode { + SpritePickingMode::AlphaThreshold(cutoff) if is_cursor_in_sprite => { let texture: &Image = images.get(&sprite.image)?; // If using a texture atlas, grab the offset of the current sprite index. (0,0) otherwise let texture_rect = sprite @@ -194,7 +196,7 @@ pub fn sprite_picking( _ => false, } } - SpriteBackendAlphaPassthrough::NoPassthrough => is_cursor_in_sprite, + SpritePickingMode::BoundingBox => is_cursor_in_sprite, _ => false, }; From 0acb495ec9664af31c4f4e3d07ccfc2fb51196c4 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Mon, 18 Nov 2024 10:21:59 +0000 Subject: [PATCH 14/28] fix typo --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 7af965ef70109..bc5de76e76f7a 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -16,7 +16,7 @@ use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; -/// How should the [`SpritePickingPlugin`] handle picking with tranparent pixels +/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels #[derive(Debug, Clone, Copy, Reflect)] pub enum SpritePickingMode { /// Even if a sprite is picked on a transparent pixel, it should still count within the backend. From 3afc5ca87b92ae92c9e97a5c926b11b03e80b490 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 21 Nov 2024 14:54:02 +0000 Subject: [PATCH 15/28] switch to using bevy internal `get_color_at` --- crates/bevy_sprite/src/picking_backend.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index bc5de76e76f7a..4759a896adc71 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -7,6 +7,7 @@ use core::cmp::Reverse; use crate::{Sprite, TextureAtlasLayout}; use bevy_app::prelude::*; use bevy_asset::prelude::*; +use bevy_color::Alpha; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_math::{prelude::*, FloatExt, FloatOrd}; @@ -23,8 +24,8 @@ pub enum SpritePickingMode { /// Only consider the rect of a given sprite. BoundingBox, /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) - /// Threshold is given as a single u8 value (0-255) representing the alpha channel that you would see in most art programs - AlphaThreshold(u8), + /// Threshold is given as an f32 representing the value you get from [bevy_color::color::Color::alpha] + AlphaThreshold(f32), } /// Runtime settings for the [`SpritePickingPlugin`]. @@ -33,14 +34,14 @@ pub enum SpritePickingMode { pub struct SpriteBackendSettings { /// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone. /// - /// Defaults to an incusive alpha threshold of 10/255 + /// Defaults to an incusive alpha threshold of 0.1 pub picking_mode: SpritePickingMode, } impl Default for SpriteBackendSettings { fn default() -> Self { Self { - picking_mode: SpritePickingMode::AlphaThreshold(10), + picking_mode: SpritePickingMode::AlphaThreshold(0.1), } } } @@ -183,15 +184,12 @@ pub fn sprite_picking( .or(Some(URect::new(0, 0, texture.width(), texture.height())))?; // get mouse position on texture let texture_position = (texture_rect.center().as_vec2() - + cursor_pos_sprite.trunc()) + + cursor_pos_sprite.floor()) .as_uvec2(); - // grab pixel - let pixel_index = - (texture_position.y * texture.width() + texture_position.x) as usize; - // check transparency - match texture.data.get(pixel_index * 4..(pixel_index * 4 + 4)) { + // grab pixel and check alpha + match texture.get_color_at(texture_position.x, texture_position.y) { // If possible check the alpha bit is above cutoff - Some(pixel_data) if pixel_data[3] > cutoff => true, + Ok(color) => color.alpha() > cutoff, // If not possible, it's not in the sprite _ => false, } From f0a90950ee4f1c95a7a89dba0363bb4114b31cbe Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 21 Nov 2024 14:57:45 +0000 Subject: [PATCH 16/28] add missing backticks --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 4759a896adc71..4d46b7bb7b6e2 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -24,7 +24,7 @@ pub enum SpritePickingMode { /// Only consider the rect of a given sprite. BoundingBox, /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) - /// Threshold is given as an f32 representing the value you get from [bevy_color::color::Color::alpha] + /// Threshold is given as an f32 representing the value you get from [`bevy_color::color::Color::alpha``] AlphaThreshold(f32), } From c9fff8954a5471799a9362e733288a5f94f385dd Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 21 Nov 2024 14:58:03 +0000 Subject: [PATCH 17/28] fix double backticks --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 4d46b7bb7b6e2..1b022e55bd32c 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -24,7 +24,7 @@ pub enum SpritePickingMode { /// Only consider the rect of a given sprite. BoundingBox, /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) - /// Threshold is given as an f32 representing the value you get from [`bevy_color::color::Color::alpha``] + /// Threshold is given as an f32 representing the value you get from [`bevy_color::color::Color::alpha`] AlphaThreshold(f32), } From c246fba556ed2a29f93f0f58a11f534cb59d954f Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Fri, 22 Nov 2024 10:08:34 +0000 Subject: [PATCH 18/28] improve accuracy and stop out of range error --- crates/bevy_sprite/src/picking_backend.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 1b022e55bd32c..fc6f0f5be420a 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -163,9 +163,11 @@ pub fn sprite_picking( } // Otherwise we can interpolate the xy of the start and end positions by the // lerp factor to get the cursor position in sprite space! + // We also need to invert the y axis as pixel data handles the y axis differently to world and screen coords let cursor_pos_sprite = cursor_start_sprite .lerp(cursor_end_sprite, lerp_factor) - .xy(); + .xy() + * Vec2::new(1.0, -1.0); let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); @@ -183,7 +185,7 @@ pub fn sprite_picking( }) .or(Some(URect::new(0, 0, texture.width(), texture.height())))?; // get mouse position on texture - let texture_position = (texture_rect.center().as_vec2() + let texture_position = (texture_rect.center().as_vec2().floor() + cursor_pos_sprite.floor()) .as_uvec2(); // grab pixel and check alpha From c64dbdf54040d18a707e1663a6a353b7c08f09d6 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Fri, 22 Nov 2024 21:45:21 +0000 Subject: [PATCH 19/28] change floor location --- crates/bevy_sprite/src/picking_backend.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index fc6f0f5be420a..308a128b47459 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -185,9 +185,10 @@ pub fn sprite_picking( }) .or(Some(URect::new(0, 0, texture.width(), texture.height())))?; // get mouse position on texture - let texture_position = (texture_rect.center().as_vec2().floor() - + cursor_pos_sprite.floor()) - .as_uvec2(); + let texture_position = (texture_rect.center().as_vec2() + + cursor_pos_sprite) + .floor() + .as_uvec2(); // grab pixel and check alpha match texture.get_color_at(texture_position.x, texture_position.y) { // If possible check the alpha bit is above cutoff From 54bdc5fab8d3ab3826ff9b06f01ca29f6471c2d5 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sat, 23 Nov 2024 02:27:49 -0800 Subject: [PATCH 20/28] Create `compute_pixel_space_point` to convert from a point relative to a sprite, to the pixel point to sample from. --- crates/bevy_sprite/src/sprite.rs | 335 ++++++++++++++++++++++++++++++- 1 file changed, 332 insertions(+), 3 deletions(-) diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index f6b8b266d57ea..3132e2ba4f8be 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -1,13 +1,13 @@ -use bevy_asset::Handle; +use bevy_asset::{Assets, Handle}; use bevy_color::Color; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_image::Image; -use bevy_math::{Rect, Vec2}; +use bevy_math::{Rect, UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{sync_world::SyncToRenderWorld, view::Visibility}; use bevy_transform::components::Transform; -use crate::{TextureAtlas, TextureSlicer}; +use crate::{TextureAtlas, TextureAtlasLayout, TextureSlicer}; /// Describes a sprite to be rendered to a 2D camera #[derive(Component, Debug, Default, Clone, Reflect)] @@ -73,6 +73,73 @@ impl Sprite { ..Default::default() } } + + /// Computes the pixel point where `point_relative_to_sprite` is sampled + /// from in this sprite. `point_relative_to_sprite` must be in the sprite's + /// local frame. Returns an Ok if the point is inside the bounds of the + /// sprite (not just the image), and returns an Err otherwise. + pub fn compute_pixel_space_point( + &self, + point_relative_to_sprite: Vec2, + images: &Assets, + texture_atlases: &Assets, + ) -> Result { + let image_size = images + .get(&self.image) + .map(|image| image.size()) + .unwrap_or(UVec2::ONE); + + let atlas_rect = self + .texture_atlas + .as_ref() + .and_then(|s| s.texture_rect(texture_atlases)) + .map(|r| r.as_rect()); + let texture_rect = match (atlas_rect, self.rect) { + (None, None) => Rect::new(0.0, 0.0, image_size.x as f32, image_size.y as f32), + (None, Some(sprite_rect)) => sprite_rect, + (Some(atlas_rect), None) => atlas_rect, + (Some(atlas_rect), Some(mut sprite_rect)) => { + // Make the sprite rect relative to the atlas rect. + sprite_rect.min += atlas_rect.min; + sprite_rect.max += atlas_rect.min; + sprite_rect + } + }; + + let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size()); + let sprite_center = -self.anchor.as_vec() * sprite_size; + + let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center; + + if self.flip_x { + point_relative_to_sprite_center.x *= -1.0; + } + // Texture coordinates start at the top left, whereas world coordinates start at the bottom + // left. So flip by default, and then don't flip if `flip_y` is set. + if !self.flip_y { + point_relative_to_sprite_center.y *= -1.0; + } + + let sprite_to_texture_ratio = { + let texture_size = texture_rect.size(); + let div_or_zero = |a, b| if b == 0.0 { 0.0 } else { a / b }; + Vec2::new( + div_or_zero(texture_size.x, sprite_size.x), + div_or_zero(texture_size.y, sprite_size.y), + ) + }; + + let point_relative_to_texture = + point_relative_to_sprite_center * sprite_to_texture_ratio + texture_rect.center(); + + // TODO: Support `SpriteImageMode`. + + if texture_rect.contains(point_relative_to_texture) { + Ok(point_relative_to_texture) + } else { + Err(point_relative_to_texture) + } + } } impl From> for Sprite { @@ -150,3 +217,265 @@ impl Anchor { } } } + +#[cfg(test)] +mod tests { + use bevy_asset::{Assets, RenderAssetUsages}; + use bevy_color::Color; + use bevy_image::Image; + use bevy_math::{Rect, URect, UVec2, Vec2}; + use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat}; + + use crate::{Anchor, TextureAtlas, TextureAtlasLayout}; + + use super::Sprite; + + /// Makes a new image of the specified size. + fn make_image(size: UVec2) -> Image { + Image::new_fill( + Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0, 0, 0, 255], + TextureFormat::Rgba8Unorm, + RenderAssetUsages::all(), + ) + } + + #[test] + fn compute_pixel_space_point_for_regular_sprite() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5))); + assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0))); + assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5))); + assert_eq!(compute(Vec2::new(3.0, 0.0)), Err(Vec2::new(5.5, 5.0))); + assert_eq!(compute(Vec2::new(-3.0, 0.0)), Err(Vec2::new(-0.5, 5.0))); + } + + #[test] + fn compute_pixel_space_point_for_color_sprite() { + let image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + // This also tests the `custom_size` field. + let sprite = Sprite::from_color(Color::BLACK, Vec2::new(50.0, 100.0)); + + let compute = |point| { + sprite + .compute_pixel_space_point(point, &image_assets, &texture_atlas_assets) + // Round to remove floating point errors. + .map(|x| (x * 1e5).round() / 1e5) + .map_err(|x| (x * 1e5).round() / 1e5) + }; + assert_eq!(compute(Vec2::new(-20.0, -40.0)), Ok(Vec2::new(0.1, 0.9))); + assert_eq!(compute(Vec2::new(0.0, 10.0)), Ok(Vec2::new(0.5, 0.4))); + assert_eq!(compute(Vec2::new(75.0, 100.0)), Err(Vec2::new(2.0, -0.5))); + assert_eq!(compute(Vec2::new(-75.0, -100.0)), Err(Vec2::new(-1.0, 1.5))); + assert_eq!(compute(Vec2::new(-30.0, -40.0)), Err(Vec2::new(-0.1, 0.9))); + } + + #[test] + fn compute_pixel_space_point_for_sprite_with_anchor_bottom_left() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + anchor: Anchor::BottomLeft, + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5))); + assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0))); + assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5))); + assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(5.5, 5.0))); + assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(-0.5, 5.0))); + } + + #[test] + fn compute_pixel_space_point_for_sprite_with_anchor_top_right() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + anchor: Anchor::TopRight, + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5))); + assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0))); + assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5))); + assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0))); + assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0))); + } + + #[test] + fn compute_pixel_space_point_for_sprite_with_anchor_flip_x() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + anchor: Anchor::BottomLeft, + flip_x: true, + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5))); + assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0))); + assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5))); + assert_eq!(compute(Vec2::new(5.5, 5.0)), Err(Vec2::new(-0.5, 5.0))); + assert_eq!(compute(Vec2::new(-0.5, 5.0)), Err(Vec2::new(5.5, 5.0))); + } + + #[test] + fn compute_pixel_space_point_for_sprite_with_anchor_flip_y() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + anchor: Anchor::TopRight, + flip_y: true, + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5))); + assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0))); + assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5))); + assert_eq!(compute(Vec2::new(0.5, -5.0)), Err(Vec2::new(5.5, 5.0))); + assert_eq!(compute(Vec2::new(-5.5, -5.0)), Err(Vec2::new(-0.5, 5.0))); + } + + #[test] + fn compute_pixel_space_point_for_sprite_with_rect() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)), + anchor: Anchor::BottomLeft, + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0))); + // The pixel is outside the rect, but is still a valid pixel in the image. + assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0))); + } + + #[test] + fn compute_pixel_space_point_for_texture_atlas_sprite() { + let mut image_assets = Assets::::default(); + let mut texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout { + size: UVec2::new(5, 10), + textures: vec![URect::new(1, 1, 4, 4)], + }); + + let sprite = Sprite { + image, + anchor: Anchor::BottomLeft, + texture_atlas: Some(TextureAtlas { + layout: texture_atlas, + index: 0, + }), + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5))); + // The pixel is outside the texture atlas, but is still a valid pixel in the image. + assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5))); + } + + #[test] + fn compute_pixel_space_point_for_texture_atlas_sprite_with_rect() { + let mut image_assets = Assets::::default(); + let mut texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout { + size: UVec2::new(5, 10), + textures: vec![URect::new(1, 1, 4, 4)], + }); + + let sprite = Sprite { + image, + anchor: Anchor::BottomLeft, + texture_atlas: Some(TextureAtlas { + layout: texture_atlas, + index: 0, + }), + // The rect is relative to the texture atlas sprite. + rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)), + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5))); + // The pixel is outside the texture atlas, but is still a valid pixel in the image. + assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5))); + } + + #[test] + fn compute_pixel_space_point_for_sprite_with_custom_size_and_rect() { + let mut image_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); + + let image = image_assets.add(make_image(UVec2::new(5, 10))); + + let sprite = Sprite { + image, + custom_size: Some(Vec2::new(100.0, 50.0)), + rect: Some(Rect::new(0.0, 0.0, 5.0, 5.0)), + ..Default::default() + }; + + let compute = + |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0))); + assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0))); + // The pixel is outside the texture atlas, but is still a valid pixel in the image. + assert_eq!(compute(Vec2::new(0.0, 35.0)), Err(Vec2::new(2.5, -1.0))); + } +} From 6b5a176d175d632761d594969bff3e00d779323a Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sat, 23 Nov 2024 02:42:44 -0800 Subject: [PATCH 21/28] Rewrite the sprite picking backend to use `Sprite::compute_pixel_space_point`. --- crates/bevy_sprite/src/picking_backend.rs | 69 ++++++++--------------- 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 308a128b47459..f8073dc586201 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -125,22 +125,6 @@ pub fn sprite_picking( return None; } - // Hit box in sprite coordinate system - let extents = match (sprite.custom_size, &sprite.texture_atlas) { - (Some(custom_size), _) => custom_size, - (None, None) => images.get(&sprite.image)?.size().as_vec2(), - (None, Some(atlas)) => texture_atlas_layout - .get(&atlas.layout) - .and_then(|layout| layout.textures.get(atlas.index)) - // Dropped atlas layouts and indexes out of bounds are rendered as a sprite - .map_or(images.get(&sprite.image)?.size().as_vec2(), |rect| { - rect.size().as_vec2() - }), - }; - let anchor = sprite.anchor.as_vec(); - let center = -anchor * extents; - let rect = Rect::from_center_half_size(center, extents / 2.0); - // Transform cursor line segment to sprite coordinate system let world_to_sprite = sprite_transform.affine().inverse(); let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin); @@ -169,36 +153,33 @@ pub fn sprite_picking( .xy() * Vec2::new(1.0, -1.0); - let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); - - let cursor_in_valid_pixels_of_sprite = match settings.picking_mode { - SpritePickingMode::AlphaThreshold(cutoff) if is_cursor_in_sprite => { - let texture: &Image = images.get(&sprite.image)?; - // If using a texture atlas, grab the offset of the current sprite index. (0,0) otherwise - let texture_rect = sprite - .texture_atlas - .as_ref() - .and_then(|atlas| { - texture_atlas_layout - .get(&atlas.layout) - .map(|f| f.textures[atlas.index]) - }) - .or(Some(URect::new(0, 0, texture.width(), texture.height())))?; - // get mouse position on texture - let texture_position = (texture_rect.center().as_vec2() - + cursor_pos_sprite) - .floor() - .as_uvec2(); - // grab pixel and check alpha - match texture.get_color_at(texture_position.x, texture_position.y) { - // If possible check the alpha bit is above cutoff - Ok(color) => color.alpha() > cutoff, - // If not possible, it's not in the sprite - _ => false, + let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point( + cursor_pos_sprite, + &images, + &texture_atlas_layout, + ) else { + return None; + }; + + let cursor_in_valid_pixels_of_sprite = 'valid_pixel: { + match settings.picking_mode { + SpritePickingMode::AlphaThreshold(cutoff) => { + let Some(image) = images.get(&sprite.image) else { + break 'valid_pixel true; + }; + // grab pixel and check alpha + let Ok(color) = image.get_color_at( + cursor_pixel_space.x as u32, + cursor_pixel_space.y as u32, + ) else { + // We don't know how to interpret the pixel. + break 'valid_pixel false; + }; + // Check the alpha is above the cutoff. + color.alpha() > cutoff } + SpritePickingMode::BoundingBox => true, } - SpritePickingMode::BoundingBox => is_cursor_in_sprite, - _ => false, }; blocked = cursor_in_valid_pixels_of_sprite From 23cc626b6fbde96f4e5b86b3bdeb0166437bacdc Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 24 Nov 2024 11:33:59 -0800 Subject: [PATCH 22/28] Add a comment to clarify that we know the cursor is in the bounds of the sprite. --- crates/bevy_sprite/src/picking_backend.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index f8073dc586201..9f1f55688f508 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -161,6 +161,9 @@ pub fn sprite_picking( return None; }; + // Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of + // the sprite. + let cursor_in_valid_pixels_of_sprite = 'valid_pixel: { match settings.picking_mode { SpritePickingMode::AlphaThreshold(cutoff) => { From f46f67cdb71515cf17bcf71321b5fdd9d639d7c0 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 24 Nov 2024 11:44:23 -0800 Subject: [PATCH 23/28] Make the sprite_picking system private, but then re-export everything in picking_backend. Previously SpriteBackendSettings was private, making it impossible to change the SpritePickingMode. Now we can! --- crates/bevy_sprite/src/lib.rs | 4 +++- crates/bevy_sprite/src/picking_backend.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index c6023b2c3055b..2d531e78590d3 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -41,6 +41,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; pub use bundle::*; pub use dynamic_texture_atlas_builder::*; pub use mesh2d::*; +#[cfg(feature = "bevy_sprite_picking_backend")] +pub use picking_backend::*; pub use render::*; pub use sprite::*; pub use texture_atlas::*; @@ -148,7 +150,7 @@ impl Plugin for SpritePlugin { #[cfg(feature = "bevy_sprite_picking_backend")] if self.add_picking { - app.add_plugins(picking_backend::SpritePickingPlugin); + app.add_plugins(SpritePickingPlugin); } if let Some(render_app) = app.get_sub_app_mut(RenderApp) { diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 9f1f55688f508..727847c2b9b65 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -57,7 +57,7 @@ impl Plugin for SpritePickingPlugin { } #[allow(clippy::too_many_arguments)] -pub fn sprite_picking( +fn sprite_picking( pointers: Query<(&PointerId, &PointerLocation)>, cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>, primary_window: Query>, From e3d6736396900a0126b397843f69b30304a30c8d Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 24 Nov 2024 12:17:41 -0800 Subject: [PATCH 24/28] Remove flipping the y-value of the cursor from the picking backend. This is already taken care of in `compute_pixel_space_point`. --- crates/bevy_sprite/src/picking_backend.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 727847c2b9b65..592d9eefe2c12 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -147,11 +147,9 @@ fn sprite_picking( } // Otherwise we can interpolate the xy of the start and end positions by the // lerp factor to get the cursor position in sprite space! - // We also need to invert the y axis as pixel data handles the y axis differently to world and screen coords let cursor_pos_sprite = cursor_start_sprite .lerp(cursor_end_sprite, lerp_factor) - .xy() - * Vec2::new(1.0, -1.0); + .xy(); let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point( cursor_pos_sprite, From f3476e04b073b60e1e454e9c755d10a2623fd1a8 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 24 Nov 2024 12:23:02 -0800 Subject: [PATCH 25/28] Rename `SpriteBackendSettings` to `SpritePickingSettings`. This matches the `SpritePickingPlugin`. --- crates/bevy_sprite/src/picking_backend.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 592d9eefe2c12..e1f24b47f164e 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -31,14 +31,14 @@ pub enum SpritePickingMode { /// Runtime settings for the [`SpritePickingPlugin`]. #[derive(Resource, Reflect)] #[reflect(Resource, Default)] -pub struct SpriteBackendSettings { +pub struct SpritePickingSettings { /// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone. /// /// Defaults to an incusive alpha threshold of 0.1 pub picking_mode: SpritePickingMode, } -impl Default for SpriteBackendSettings { +impl Default for SpritePickingSettings { fn default() -> Self { Self { picking_mode: SpritePickingMode::AlphaThreshold(0.1), @@ -51,7 +51,7 @@ pub struct SpritePickingPlugin; impl Plugin for SpritePickingPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + app.init_resource::() .add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend)); } } @@ -63,7 +63,7 @@ fn sprite_picking( primary_window: Query>, images: Res>, texture_atlas_layout: Res>, - settings: Res, + settings: Res, sprite_query: Query<( Entity, &Sprite, From 22e7547f7f85c47122bd60149af5e021c23c4305 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 28 Nov 2024 09:34:49 +0000 Subject: [PATCH 26/28] remove un-needed closure --- crates/bevy_sprite/src/sprite.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 3132e2ba4f8be..5305d023b4138 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -86,7 +86,7 @@ impl Sprite { ) -> Result { let image_size = images .get(&self.image) - .map(|image| image.size()) + .map(Image::size) .unwrap_or(UVec2::ONE); let atlas_rect = self From 64f1cf7a85aab12d0ed5fbb6658166a781f22a91 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Thu, 28 Nov 2024 10:04:41 +0000 Subject: [PATCH 27/28] fix comments --- crates/bevy_sprite/src/picking_backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index e1f24b47f164e..130efc6646323 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -24,7 +24,7 @@ pub enum SpritePickingMode { /// Only consider the rect of a given sprite. BoundingBox, /// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive) - /// Threshold is given as an f32 representing the value you get from [`bevy_color::color::Color::alpha`] + /// Threshold is given as an f32 representing the alpha value in a Bevy Color Value AlphaThreshold(f32), } From 8bff88aa2d48785286b805bf51e5090328b440f4 Mon Sep 17 00:00:00 2001 From: Michael Walter Van Der Velden Date: Mon, 2 Dec 2024 12:46:41 +0000 Subject: [PATCH 28/28] added @andriyDev's comment to code --- crates/bevy_sprite/src/picking_backend.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 130efc6646323..bd57aaf202036 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -166,6 +166,8 @@ fn sprite_picking( match settings.picking_mode { SpritePickingMode::AlphaThreshold(cutoff) => { let Some(image) = images.get(&sprite.image) else { + // [`Sprite::from_color`] returns a defaulted handle. + // This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible break 'valid_pixel true; }; // grab pixel and check alpha