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