Path: blob/main/crates/bevy_sprite/src/picking_backend.rs
9368 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::{16visibility::{RenderLayers, ViewVisibility},17Camera, Projection, RenderTarget,18};19use bevy_color::Alpha;20use bevy_ecs::prelude::*;21use bevy_image::{prelude::*, TextureAccessError};22use bevy_log::warn;23use bevy_math::{prelude::*, FloatExt};24use bevy_picking::backend::prelude::*;25use bevy_reflect::prelude::*;26use bevy_transform::prelude::*;27use bevy_window::PrimaryWindow;2829/// An optional component that marks cameras that should be used in the [`SpritePickingPlugin`].30///31/// Only needed if [`SpritePickingSettings::require_markers`] is set to `true`, and ignored32/// otherwise.33#[derive(Debug, Clone, Default, Component, Reflect)]34#[reflect(Debug, Default, Component, Clone)]35pub struct SpritePickingCamera;3637/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels38#[derive(Debug, Clone, Copy, Reflect)]39#[reflect(Debug, Clone)]40pub enum SpritePickingMode {41/// Even if a sprite is picked on a transparent pixel, it should still count within the backend.42/// Only consider the rect of a given sprite.43BoundingBox,44/// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive)45/// Threshold is given as an f32 representing the alpha value in a Bevy Color Value46AlphaThreshold(f32),47}4849/// Runtime settings for the [`SpritePickingPlugin`].50#[derive(Resource, Reflect)]51#[reflect(Resource, Default)]52pub struct SpritePickingSettings {53/// When set to `true` sprite picking will only consider cameras marked with54/// [`SpritePickingCamera`]. Defaults to `false`.55/// Regardless of this setting, only sprites marked with [`Pickable`] will be considered.56///57/// This setting is provided to give you fine-grained control over which cameras58/// should be used by the sprite picking backend at runtime.59pub require_markers: bool,60/// 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.61///62/// Defaults to an inclusive alpha threshold of 0.163pub picking_mode: SpritePickingMode,64}6566impl Default for SpritePickingSettings {67fn default() -> Self {68Self {69require_markers: false,70picking_mode: SpritePickingMode::AlphaThreshold(0.1),71}72}73}7475/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.76#[derive(Clone)]77pub struct SpritePickingPlugin;7879impl Plugin for SpritePickingPlugin {80fn build(&self, app: &mut App) {81app.init_resource::<SpritePickingSettings>()82.add_systems(PreUpdate, sprite_picking.in_set(PickingSystems::Backend));83}84}8586fn sprite_picking(87pointers: Query<(&PointerId, &PointerLocation)>,88cameras: Query<(89Entity,90&Camera,91&RenderTarget,92&GlobalTransform,93&Projection,94Has<SpritePickingCamera>,95Option<&RenderLayers>,96)>,97primary_window: Query<Entity, With<PrimaryWindow>>,98images: Res<Assets<Image>>,99texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,100settings: Res<SpritePickingSettings>,101sprite_query: Query<(102Entity,103&Sprite,104&GlobalTransform,105&Anchor,106&Pickable,107&ViewVisibility,108Option<&RenderLayers>,109)>,110mut pointer_hits_writer: MessageWriter<PointerHits>,111ray_map: Res<RayMap>,112) {113let mut sorted_sprites: Vec<_> = sprite_query114.iter()115.filter_map(116|(entity, sprite, transform, anchor, pickable, vis, render_layers)| {117if !transform.affine().is_nan() && vis.get() {118Some((entity, sprite, transform, anchor, pickable, render_layers))119} else {120None121}122},123)124.collect();125126// radsort is a stable radix sort that performed better than `slice::sort_by_key`127radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _, _)| {128-transform.translation().z129});130131let primary_window = primary_window.single().ok();132133let pick_sets = ray_map.iter().flat_map(|(ray_id, ray)| {134let mut blocked = false;135136let Ok((137cam_entity,138camera,139render_target,140cam_transform,141Projection::Orthographic(cam_ortho),142cam_can_pick,143cam_render_layers,144)) = cameras.get(ray_id.camera)145else {146return None;147};148149let marker_requirement = !settings.require_markers || cam_can_pick;150if !camera.is_active || !marker_requirement {151return None;152}153154let location = pointers.iter().find_map(|(id, loc)| {155if *id == ray_id.pointer {156return loc.location.as_ref();157}158None159})?;160161if render_target162.normalize(primary_window)163.is_none_or(|x| x != location.target)164{165return None;166}167168let viewport_pos = location.position;169if let Some(viewport) = camera.logical_viewport_rect()170&& !viewport.contains(viewport_pos)171{172// The pointer is outside the viewport, skip it173return None;174}175176let cursor_ray_len = cam_ortho.far - cam_ortho.near;177let cursor_ray_end = ray.origin + ray.direction * cursor_ray_len;178179let picks: Vec<(Entity, HitData)> = sorted_sprites180.iter()181.copied()182.filter_map(183|(entity, sprite, sprite_transform, anchor, pickable, sprite_render_layers)| {184if blocked {185return None;186}187188// Filter out sprites based on whether they share RenderLayers with the current189// ray's associated camera.190// Any entity without a RenderLayers component will by default be191// on RenderLayers::layer(0) only.192if !cam_render_layers193.unwrap_or_default()194.intersects(sprite_render_layers.unwrap_or_default())195{196return None;197}198199// Transform cursor line segment to sprite coordinate system200let world_to_sprite = sprite_transform.affine().inverse();201let cursor_start_sprite = world_to_sprite.transform_point3(ray.origin);202let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);203204// Find where the cursor segment intersects the plane Z=0 (which is the sprite's205// plane in sprite-local space). It may not intersect if, for example, we're206// viewing the sprite side-on207if cursor_start_sprite.z == cursor_end_sprite.z {208// Cursor ray is parallel to the sprite and misses it209return None;210}211let lerp_factor =212f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);213if !(0.0..=1.0).contains(&lerp_factor) {214// Lerp factor is out of range, meaning that while an infinite line cast by215// the cursor would intersect the sprite, the sprite is not between the216// camera's near and far planes217return None;218}219// Otherwise we can interpolate the xy of the start and end positions by the220// lerp factor to get the cursor position in sprite space!221let cursor_pos_sprite = cursor_start_sprite222.lerp(cursor_end_sprite, lerp_factor)223.xy();224225let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(226cursor_pos_sprite,227*anchor,228&images,229&texture_atlas_layout,230) else {231return None;232};233234// Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of235// the sprite.236237let cursor_in_valid_pixels_of_sprite = 'valid_pixel: {238match settings.picking_mode {239SpritePickingMode::AlphaThreshold(cutoff) => {240let Some(image) = images.get(&sprite.image) else {241// [`Sprite::from_color`] returns a defaulted handle.242// This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible243break 'valid_pixel true;244};245// grab pixel and check alpha246let color = match image.get_color_at(247cursor_pixel_space.x as u32,248cursor_pixel_space.y as u32,249) {250Ok(color) => color,251Err(TextureAccessError::UnsupportedTextureFormat(format)) => {252warn!(253"Failed to get pixel color for sprite picking on entity {:?}: unsupported texture format {:?}. \254This is often caused by the use of a compressed texture format. \255Consider using `SpritePickingMode::BoundingBox`.",256entity,257format258);259break 'valid_pixel false;260}261Err(_) => break 'valid_pixel false,262};263// Check the alpha is above the cutoff.264color.alpha() > cutoff265}266SpritePickingMode::BoundingBox => true,267}268};269270blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;271272cursor_in_valid_pixels_of_sprite.then(|| {273let hit_pos_world =274sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));275// Transform point from world to camera space to get the Z distance276let hit_pos_cam = cam_transform277.affine()278.inverse()279.transform_point3(hit_pos_world);280// HitData requires a depth as calculated from the camera's near clipping plane281let depth = -cam_ortho.near - hit_pos_cam.z;282(283entity,284HitData::new(285cam_entity,286depth,287Some(hit_pos_world),288Some(*sprite_transform.back()),289),290)291})292},293)294.collect();295296Some((ray_id.pointer, picks, camera.order))297});298299pick_sets.for_each(|(pointer, picks, order)| {300pointer_hits_writer.write(PointerHits::new(pointer, picks, order as f32));301});302}303304305