Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_sprite/src/picking_backend.rs
6598 views
1
//! A [`bevy_picking`] backend for sprites. Works for simple sprites and sprite atlases. Works for
2
//! sprites with arbitrary transforms.
3
//!
4
//! By default, picking for sprites is based on pixel opacity.
5
//! A sprite is picked only when a pointer is over an opaque pixel.
6
//! Alternatively, you can configure picking to be based on sprite bounds.
7
//!
8
//! ## Implementation Notes
9
//!
10
//! - The `position` reported in `HitData` in world space, and the `normal` is a normalized
11
//! vector provided by the target's `GlobalTransform::back()`.
12
13
use crate::{Anchor, Sprite};
14
use bevy_app::prelude::*;
15
use bevy_asset::prelude::*;
16
use bevy_camera::{visibility::ViewVisibility, Camera, Projection};
17
use bevy_color::Alpha;
18
use bevy_ecs::prelude::*;
19
use bevy_image::prelude::*;
20
use bevy_math::{prelude::*, FloatExt};
21
use bevy_picking::backend::prelude::*;
22
use bevy_reflect::prelude::*;
23
use bevy_transform::prelude::*;
24
use bevy_window::PrimaryWindow;
25
26
/// An optional component that marks cameras that should be used in the [`SpritePickingPlugin`].
27
///
28
/// Only needed if [`SpritePickingSettings::require_markers`] is set to `true`, and ignored
29
/// otherwise.
30
#[derive(Debug, Clone, Default, Component, Reflect)]
31
#[reflect(Debug, Default, Component, Clone)]
32
pub struct SpritePickingCamera;
33
34
/// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels
35
#[derive(Debug, Clone, Copy, Reflect)]
36
#[reflect(Debug, Clone)]
37
pub enum SpritePickingMode {
38
/// Even if a sprite is picked on a transparent pixel, it should still count within the backend.
39
/// Only consider the rect of a given sprite.
40
BoundingBox,
41
/// Ignore any part of a sprite which has a lower alpha value than the threshold (inclusive)
42
/// Threshold is given as an f32 representing the alpha value in a Bevy Color Value
43
AlphaThreshold(f32),
44
}
45
46
/// Runtime settings for the [`SpritePickingPlugin`].
47
#[derive(Resource, Reflect)]
48
#[reflect(Resource, Default)]
49
pub struct SpritePickingSettings {
50
/// When set to `true` sprite picking will only consider cameras marked with
51
/// [`SpritePickingCamera`]. Defaults to `false`.
52
/// Regardless of this setting, only sprites marked with [`Pickable`] will be considered.
53
///
54
/// This setting is provided to give you fine-grained control over which cameras
55
/// should be used by the sprite picking backend at runtime.
56
pub require_markers: bool,
57
/// 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.
58
///
59
/// Defaults to an inclusive alpha threshold of 0.1
60
pub picking_mode: SpritePickingMode,
61
}
62
63
impl Default for SpritePickingSettings {
64
fn default() -> Self {
65
Self {
66
require_markers: false,
67
picking_mode: SpritePickingMode::AlphaThreshold(0.1),
68
}
69
}
70
}
71
72
/// Enables the sprite picking backend, allowing you to click on, hover over and drag sprites.
73
#[derive(Clone)]
74
pub struct SpritePickingPlugin;
75
76
impl Plugin for SpritePickingPlugin {
77
fn build(&self, app: &mut App) {
78
app.init_resource::<SpritePickingSettings>()
79
.add_systems(PreUpdate, sprite_picking.in_set(PickingSystems::Backend));
80
}
81
}
82
83
fn sprite_picking(
84
pointers: Query<(&PointerId, &PointerLocation)>,
85
cameras: Query<(
86
Entity,
87
&Camera,
88
&GlobalTransform,
89
&Projection,
90
Has<SpritePickingCamera>,
91
)>,
92
primary_window: Query<Entity, With<PrimaryWindow>>,
93
images: Res<Assets<Image>>,
94
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
95
settings: Res<SpritePickingSettings>,
96
sprite_query: Query<(
97
Entity,
98
&Sprite,
99
&GlobalTransform,
100
&Anchor,
101
&Pickable,
102
&ViewVisibility,
103
)>,
104
mut output: EventWriter<PointerHits>,
105
) {
106
let mut sorted_sprites: Vec<_> = sprite_query
107
.iter()
108
.filter_map(|(entity, sprite, transform, anchor, pickable, vis)| {
109
if !transform.affine().is_nan() && vis.get() {
110
Some((entity, sprite, transform, anchor, pickable))
111
} else {
112
None
113
}
114
})
115
.collect();
116
117
// radsort is a stable radix sort that performed better than `slice::sort_by_key`
118
radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _)| {
119
-transform.translation().z
120
});
121
122
let primary_window = primary_window.single().ok();
123
124
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
125
pointer_location.location().map(|loc| (pointer, loc))
126
}) {
127
let mut blocked = false;
128
let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho), _)) =
129
cameras
130
.iter()
131
.filter(|(_, camera, _, _, cam_can_pick)| {
132
let marker_requirement = !settings.require_markers || *cam_can_pick;
133
camera.is_active && marker_requirement
134
})
135
.find(|(_, camera, _, _, _)| {
136
camera
137
.target
138
.normalize(primary_window)
139
.is_some_and(|x| x == location.target)
140
})
141
else {
142
continue;
143
};
144
145
let viewport_pos = location.position;
146
if let Some(viewport) = camera.logical_viewport_rect()
147
&& !viewport.contains(viewport_pos)
148
{
149
// The pointer is outside the viewport, skip it
150
continue;
151
}
152
153
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, viewport_pos) else {
154
continue;
155
};
156
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
157
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
158
159
let picks: Vec<(Entity, HitData)> = sorted_sprites
160
.iter()
161
.copied()
162
.filter_map(|(entity, sprite, sprite_transform, anchor, pickable)| {
163
if blocked {
164
return None;
165
}
166
167
// Transform cursor line segment to sprite coordinate system
168
let world_to_sprite = sprite_transform.affine().inverse();
169
let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
170
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
171
172
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
173
// plane in sprite-local space). It may not intersect if, for example, we're
174
// viewing the sprite side-on
175
if cursor_start_sprite.z == cursor_end_sprite.z {
176
// Cursor ray is parallel to the sprite and misses it
177
return None;
178
}
179
let lerp_factor =
180
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
181
if !(0.0..=1.0).contains(&lerp_factor) {
182
// Lerp factor is out of range, meaning that while an infinite line cast by
183
// the cursor would intersect the sprite, the sprite is not between the
184
// camera's near and far planes
185
return None;
186
}
187
// Otherwise we can interpolate the xy of the start and end positions by the
188
// lerp factor to get the cursor position in sprite space!
189
let cursor_pos_sprite = cursor_start_sprite
190
.lerp(cursor_end_sprite, lerp_factor)
191
.xy();
192
193
let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point(
194
cursor_pos_sprite,
195
*anchor,
196
&images,
197
&texture_atlas_layout,
198
) else {
199
return None;
200
};
201
202
// Since the pixel space coordinate is `Ok`, we know the cursor is in the bounds of
203
// the sprite.
204
205
let cursor_in_valid_pixels_of_sprite = 'valid_pixel: {
206
match settings.picking_mode {
207
SpritePickingMode::AlphaThreshold(cutoff) => {
208
let Some(image) = images.get(&sprite.image) else {
209
// [`Sprite::from_color`] returns a defaulted handle.
210
// This handle doesn't return a valid image, so returning false here would make picking "color sprites" impossible
211
break 'valid_pixel true;
212
};
213
// grab pixel and check alpha
214
let Ok(color) = image.get_color_at(
215
cursor_pixel_space.x as u32,
216
cursor_pixel_space.y as u32,
217
) else {
218
// We don't know how to interpret the pixel.
219
break 'valid_pixel false;
220
};
221
// Check the alpha is above the cutoff.
222
color.alpha() > cutoff
223
}
224
SpritePickingMode::BoundingBox => true,
225
}
226
};
227
228
blocked = cursor_in_valid_pixels_of_sprite && pickable.should_block_lower;
229
230
cursor_in_valid_pixels_of_sprite.then(|| {
231
let hit_pos_world =
232
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
233
// Transform point from world to camera space to get the Z distance
234
let hit_pos_cam = cam_transform
235
.affine()
236
.inverse()
237
.transform_point3(hit_pos_world);
238
// HitData requires a depth as calculated from the camera's near clipping plane
239
let depth = -cam_ortho.near - hit_pos_cam.z;
240
(
241
entity,
242
HitData::new(
243
cam_entity,
244
depth,
245
Some(hit_pos_world),
246
Some(*sprite_transform.back()),
247
),
248
)
249
})
250
})
251
.collect();
252
253
let order = camera.order as f32;
254
output.write(PointerHits::new(*pointer, picks, order));
255
}
256
}
257
258