use crate::{Anchor, Sprite};
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_camera::{
visibility::{RenderLayers, ViewVisibility},
Camera, Projection, RenderTarget,
};
use bevy_color::Alpha;
use bevy_ecs::prelude::*;
use bevy_image::prelude::*;
use bevy_math::{prelude::*, FloatExt};
use bevy_picking::backend::prelude::*;
use bevy_reflect::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Debug, Default, Component, Clone)]
pub struct SpritePickingCamera;
#[derive(Debug, Clone, Copy, Reflect)]
#[reflect(Debug, Clone)]
pub enum SpritePickingMode {
BoundingBox,
AlphaThreshold(f32),
}
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct SpritePickingSettings {
pub require_markers: bool,
pub picking_mode: SpritePickingMode,
}
impl Default for SpritePickingSettings {
fn default() -> Self {
Self {
require_markers: false,
picking_mode: SpritePickingMode::AlphaThreshold(0.1),
}
}
}
#[derive(Clone)]
pub struct SpritePickingPlugin;
impl Plugin for SpritePickingPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SpritePickingSettings>()
.add_systems(PreUpdate, sprite_picking.in_set(PickingSystems::Backend));
}
}
fn sprite_picking(
pointers: Query<(&PointerId, &PointerLocation)>,
cameras: Query<(
Entity,
&Camera,
&RenderTarget,
&GlobalTransform,
&Projection,
Has<SpritePickingCamera>,
Option<&RenderLayers>,
)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
images: Res<Assets<Image>>,
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
settings: Res<SpritePickingSettings>,
sprite_query: Query<(
Entity,
&Sprite,
&GlobalTransform,
&Anchor,
&Pickable,
&ViewVisibility,
Option<&RenderLayers>,
)>,
mut pointer_hits_writer: MessageWriter<PointerHits>,
ray_map: Res<RayMap>,
) {
let mut sorted_sprites: Vec<_> = sprite_query
.iter()
.filter_map(
|(entity, sprite, transform, anchor, pickable, vis, render_layers)| {
if !transform.affine().is_nan() && vis.get() {
Some((entity, sprite, transform, anchor, pickable, render_layers))
} else {
None
}
},
)
.collect();
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _, _)| {
-transform.translation().z
});
let primary_window = primary_window.single().ok();
let pick_sets = ray_map.iter().flat_map(|(ray_id, ray)| {
let mut blocked = false;
let Ok((
cam_entity,
camera,
render_target,
cam_transform,
Projection::Orthographic(cam_ortho),
cam_can_pick,
cam_render_layers,
)) = cameras.get(ray_id.camera)
else {
return None;
};
let marker_requirement = !settings.require_markers || cam_can_pick;
if !camera.is_active || !marker_requirement {
return None;
}
let location = pointers.iter().find_map(|(id, loc)| {
if *id == ray_id.pointer {
return loc.location.as_ref();
}
None
})?;
if render_target
.normalize(primary_window)
.is_none_or(|x| x != location.target)
{
return None;
}
let viewport_pos = location.position;
if let Some(viewport) = camera.logical_viewport_rect()
&& !viewport.contains(viewport_pos)
{
return None;
}
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
let cursor_ray_end = ray.origin + ray.direction * cursor_ray_len;
let picks: Vec<(Entity, HitData)> = sorted_sprites
.iter()
.copied()
.filter_map(
|(entity, sprite, sprite_transform, anchor, pickable, sprite_render_layers)| {
if blocked {
return None;
}
if !cam_render_layers
.unwrap_or_default()
.intersects(sprite_render_layers.unwrap_or_default())
{
return None;
}
let world_to_sprite = sprite_transform.affine().inverse();
let cursor_start_sprite = world_to_sprite.transform_point3(ray.origin);
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
if cursor_start_sprite.z == cursor_end_sprite.z {
return None;
}
let lerp_factor =
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
if !(0.0..=1.0).contains(&lerp_factor) {
return None;
}
let cursor_pos_sprite = cursor_start_sprite
.lerp(cursor_end_sprite, lerp_factor)
.xy();
let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
cursor_pos_sprite,
*anchor,
&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;
};
let Ok(color) = image.get_color_at(
cursor_pixel_space.x as u32,
cursor_pixel_space.y as u32,
) else {
break 'valid_pixel false;
};
color.alpha() > cutoff
}
SpritePickingMode::BoundingBox => true,
}
};
blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;
cursor_in_valid_pixels_of_sprite.then(|| {
let hit_pos_world =
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
let hit_pos_cam = cam_transform
.affine()
.inverse()
.transform_point3(hit_pos_world);
let depth = -cam_ortho.near - hit_pos_cam.z;
(
entity,
HitData::new(
cam_entity,
depth,
Some(hit_pos_world),
Some(*sprite_transform.back()),
),
)
})
},
)
.collect();
Some((ray_id.pointer, picks, camera.order))
});
pick_sets.for_each(|(pointer, picks, order)| {
pointer_hits_writer.write(PointerHits::new(pointer, picks, order as f32));
});
}