Path: blob/main/crates/bevy_sprite/src/picking_backend.rs
6598 views
//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for1//! sprites with arbitrary transforms.2//!3//! By default, picking for sprites is based on pixel opacity.4//! A sprite is picked only when a pointer is over an opaque pixel.5//! Alternatively, you can configure picking to be based on sprite bounds.6//!7//! ## Implementation Notes8//!9//! - The `position` reported in `HitData` in world space, and the `normal` is a normalized10//! vector provided by the target's `GlobalTransform::back()`.1112use crate::{Anchor, Sprite};13use bevy_app::prelude::*;14use bevy_asset::prelude::*;15use bevy_camera::{visibility::ViewVisibility, Camera, Projection};16use bevy_color::Alpha;17use bevy_ecs::prelude::*;18use bevy_image::prelude::*;19use bevy_math::{prelude::*, FloatExt};20use bevy_picking::backend::prelude::*;21use bevy_reflect::prelude::*;22use bevy_transform::prelude::*;23use bevy_window::PrimaryWindow;2425/// An optional component that marks cameras that should be used in the [`SpritePickingPlugin`].26///27/// Only needed if [`SpritePickingSettings::require_markers`] is set to `true`, and ignored28/// otherwise.29#[derive(Debug, Clone, Default, Component, Reflect)]30#[reflect(Debug, Default, Component, Clone)]31pub struct SpritePickingCamera;3233/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels34#[derive(Debug, Clone, Copy, Reflect)]35#[reflect(Debug, Clone)]36pub enum SpritePickingMode {37/// Even if a sprite is picked on a transparent pixel, it should still count within the backend.38/// Only consider the rect of a given sprite.39BoundingBox,40/// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive)41/// Threshold is given as an f32 representing the alpha value in a Bevy Color Value42AlphaThreshold(f32),43}4445/// Runtime settings for the [`SpritePickingPlugin`].46#[derive(Resource, Reflect)]47#[reflect(Resource, Default)]48pub struct SpritePickingSettings {49/// When set to `true` sprite picking will only consider cameras marked with50/// [`SpritePickingCamera`]. Defaults to `false`.51/// Regardless of this setting, only sprites marked with [`Pickable`] will be considered.52///53/// This setting is provided to give you fine-grained control over which cameras54/// should be used by the sprite picking backend at runtime.55pub require_markers: bool,56/// 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.57///58/// Defaults to an inclusive alpha threshold of 0.159pub picking_mode: SpritePickingMode,60}6162impl Default for SpritePickingSettings {63fn default() -> Self {64Self {65require_markers: false,66picking_mode: SpritePickingMode::AlphaThreshold(0.1),67}68}69}7071/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.72#[derive(Clone)]73pub struct SpritePickingPlugin;7475impl Plugin for SpritePickingPlugin {76fn build(&self, app: &mut App) {77app.init_resource::<SpritePickingSettings>()78.add_systems(PreUpdate, sprite_picking.in_set(PickingSystems::Backend));79}80}8182fn sprite_picking(83pointers: Query<(&PointerId, &PointerLocation)>,84cameras: Query<(85Entity,86&Camera,87&GlobalTransform,88&Projection,89Has<SpritePickingCamera>,90)>,91primary_window: Query<Entity, With<PrimaryWindow>>,92images: Res<Assets<Image>>,93texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,94settings: Res<SpritePickingSettings>,95sprite_query: Query<(96Entity,97&Sprite,98&GlobalTransform,99&Anchor,100&Pickable,101&ViewVisibility,102)>,103mut output: EventWriter<PointerHits>,104) {105let mut sorted_sprites: Vec<_> = sprite_query106.iter()107.filter_map(|(entity, sprite, transform, anchor, pickable, vis)| {108if !transform.affine().is_nan() && vis.get() {109Some((entity, sprite, transform, anchor, pickable))110} else {111None112}113})114.collect();115116// radsort is a stable radix sort that performed better than `slice::sort_by_key`117radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _)| {118-transform.translation().z119});120121let primary_window = primary_window.single().ok();122123for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {124pointer_location.location().map(|loc| (pointer, loc))125}) {126let mut blocked = false;127let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho), _)) =128cameras129.iter()130.filter(|(_, camera, _, _, cam_can_pick)| {131let marker_requirement = !settings.require_markers || *cam_can_pick;132camera.is_active && marker_requirement133})134.find(|(_, camera, _, _, _)| {135camera136.target137.normalize(primary_window)138.is_some_and(|x| x == location.target)139})140else {141continue;142};143144let viewport_pos = location.position;145if let Some(viewport) = camera.logical_viewport_rect()146&& !viewport.contains(viewport_pos)147{148// The pointer is outside the viewport, skip it149continue;150}151152let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, viewport_pos) else {153continue;154};155let cursor_ray_len = cam_ortho.far - cam_ortho.near;156let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;157158let picks: Vec<(Entity, HitData)> = sorted_sprites159.iter()160.copied()161.filter_map(|(entity, sprite, sprite_transform, anchor, pickable)| {162if blocked {163return None;164}165166// Transform cursor line segment to sprite coordinate system167let world_to_sprite = sprite_transform.affine().inverse();168let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);169let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);170171// Find where the cursor segment intersects the plane Z=0 (which is the sprite's172// plane in sprite-local space). It may not intersect if, for example, we're173// viewing the sprite side-on174if cursor_start_sprite.z == cursor_end_sprite.z {175// Cursor ray is parallel to the sprite and misses it176return None;177}178let lerp_factor =179f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);180if !(0.0..=1.0).contains(&lerp_factor) {181// Lerp factor is out of range, meaning that while an infinite line cast by182// the cursor would intersect the sprite, the sprite is not between the183// camera's near and far planes184return None;185}186// Otherwise we can interpolate the xy of the start and end positions by the187// lerp factor to get the cursor position in sprite space!188let cursor_pos_sprite = cursor_start_sprite189.lerp(cursor_end_sprite, lerp_factor)190.xy();191192let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(193cursor_pos_sprite,194*anchor,195&images,196&texture_atlas_layout,197) else {198return None;199};200201// Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of202// the sprite.203204let cursor_in_valid_pixels_of_sprite = 'valid_pixel: {205match settings.picking_mode {206SpritePickingMode::AlphaThreshold(cutoff) => {207let Some(image) = images.get(&sprite.image) else {208// [`Sprite::from_color`] returns a defaulted handle.209// This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible210break 'valid_pixel true;211};212// grab pixel and check alpha213let Ok(color) = image.get_color_at(214cursor_pixel_space.x as u32,215cursor_pixel_space.y as u32,216) else {217// We don't know how to interpret the pixel.218break 'valid_pixel false;219};220// Check the alpha is above the cutoff.221color.alpha() > cutoff222}223SpritePickingMode::BoundingBox => true,224}225};226227blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;228229cursor_in_valid_pixels_of_sprite.then(|| {230let hit_pos_world =231sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));232// Transform point from world to camera space to get the Z distance233let hit_pos_cam = cam_transform234.affine()235.inverse()236.transform_point3(hit_pos_world);237// HitData requires a depth as calculated from the camera's near clipping plane238let depth = -cam_ortho.near - hit_pos_cam.z;239(240entity,241HitData::new(242cam_entity,243depth,244Some(hit_pos_world),245Some(*sprite_transform.back()),246),247)248})249})250.collect();251252let order = camera.order as f32;253output.write(PointerHits::new(*pointer, picks, order));254}255}256257258