Skip to content

Commit 5e820c8

Browse files
committed
Copy/paste sprite picking backend
1 parent 9ca5540 commit 5e820c8

File tree

4 files changed

+190
-1
lines changed

4 files changed

+190
-1
lines changed

crates/bevy_internal/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
191191
bevy_dev_tools = ["dep:bevy_dev_tools"]
192192

193193
# Provides a picking functionality
194-
bevy_picking = ["dep:bevy_picking", "bevy_ui?/bevy_picking"]
194+
bevy_picking = [
195+
"dep:bevy_picking",
196+
"bevy_ui?/bevy_picking",
197+
"bevy_sprite?/bevy_picking",
198+
]
195199

196200
# Enable support for the ios_simulator by downgrading some rendering capabilities
197201
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]

crates/bevy_sprite/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0"
99
keywords = ["bevy"]
1010

1111
[features]
12+
bevy_picking = ["dep:bevy_picking", "dep:bevy_window"]
1213
webgl = []
1314
webgpu = []
1415

@@ -20,12 +21,14 @@ bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
2021
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" }
2122
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
2223
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
24+
bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true }
2325
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
2426
"bevy",
2527
] }
2628
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
2729
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
2830
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
31+
bevy_window = { path = "../bevy_window", version = "0.15.0-dev", optional = true }
2932
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
3033

3134
# other

crates/bevy_sprite/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
mod bundle;
1212
mod dynamic_texture_atlas_builder;
1313
mod mesh2d;
14+
#[cfg(feature = "bevy_picking")]
15+
mod picking_backend;
1416
mod render;
1517
mod sprite;
1618
mod texture_atlas;
@@ -133,6 +135,9 @@ impl Plugin for SpritePlugin {
133135
),
134136
);
135137

138+
#[cfg(feature = "bevy_picking")]
139+
app.add_plugins(picking_backend::SpriteBackend);
140+
136141
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
137142
render_app
138143
.init_resource::<ImageBindGroups>()
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! A [`bevy_picking`] backend for sprites.
2+
3+
use std::cmp::Ordering;
4+
5+
use crate::{Sprite, TextureAtlas, TextureAtlasLayout};
6+
use bevy_app::prelude::*;
7+
use bevy_asset::prelude::*;
8+
use bevy_ecs::prelude::*;
9+
use bevy_math::prelude::*;
10+
use bevy_picking::backend::prelude::*;
11+
use bevy_render::prelude::*;
12+
use bevy_transform::prelude::*;
13+
use bevy_window::PrimaryWindow;
14+
15+
#[derive(Clone)]
16+
pub struct SpriteBackend;
17+
18+
impl Plugin for SpriteBackend {
19+
fn build(&self, app: &mut App) {
20+
app.add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend));
21+
}
22+
}
23+
24+
pub fn sprite_picking(
25+
pointers: Query<(&PointerId, &PointerLocation)>,
26+
cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>,
27+
primary_window: Query<Entity, With<PrimaryWindow>>,
28+
images: Res<Assets<Image>>,
29+
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
30+
sprite_query: Query<
31+
(
32+
Entity,
33+
Option<&Sprite>,
34+
Option<&TextureAtlas>,
35+
Option<&Handle<Image>>,
36+
&GlobalTransform,
37+
Option<&Pickable>,
38+
&ViewVisibility,
39+
),
40+
Or<(With<Sprite>, With<TextureAtlas>)>,
41+
>,
42+
mut output: EventWriter<PointerHits>,
43+
) {
44+
let mut sorted_sprites: Vec<_> = sprite_query.iter().collect();
45+
sorted_sprites.sort_by(|a, b| {
46+
(b.4.translation().z)
47+
.partial_cmp(&a.4.translation().z)
48+
.unwrap_or(Ordering::Equal)
49+
});
50+
51+
for (pointer, location) in pointers.iter().filter_map(|(pointer, pointer_location)| {
52+
pointer_location.location().map(|loc| (pointer, loc))
53+
}) {
54+
let mut blocked = false;
55+
let Some((cam_entity, camera, cam_transform, cam_ortho)) = cameras
56+
.iter()
57+
.filter(|(_, camera, _, _)| camera.is_active)
58+
.find(|(_, camera, _, _)| {
59+
camera
60+
.target
61+
.normalize(Some(match primary_window.get_single() {
62+
Ok(w) => w,
63+
Err(_) => return false,
64+
}))
65+
.unwrap()
66+
== location.target
67+
})
68+
else {
69+
continue;
70+
};
71+
72+
let Some(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position)
73+
else {
74+
continue;
75+
};
76+
let cursor_ray_len = cam_ortho.far - cam_ortho.near;
77+
let cursor_ray_end = cursor_ray_world.origin + cursor_ray_world.direction * cursor_ray_len;
78+
79+
let picks: Vec<(Entity, HitData)> = sorted_sprites
80+
.iter()
81+
.copied()
82+
.filter(|(.., visibility)| visibility.get())
83+
.filter_map(
84+
|(entity, sprite, atlas, image, sprite_transform, pickable, ..)| {
85+
if blocked {
86+
return None;
87+
}
88+
89+
// Hit box in sprite coordinate system
90+
let (extents, anchor) = if let Some((sprite, atlas)) = sprite.zip(atlas) {
91+
let extents = sprite.custom_size.or_else(|| {
92+
texture_atlas_layout
93+
.get(&atlas.layout)
94+
.map(|f| f.textures[atlas.index].size().as_vec2())
95+
})?;
96+
let anchor = sprite.anchor.as_vec();
97+
(extents, anchor)
98+
} else if let Some((sprite, image)) = sprite.zip(image) {
99+
let extents = sprite
100+
.custom_size
101+
.or_else(|| images.get(image).map(|f| f.size().as_vec2()))?;
102+
let anchor = sprite.anchor.as_vec();
103+
(extents, anchor)
104+
} else {
105+
return None;
106+
};
107+
108+
let center = -anchor * extents;
109+
let rect = Rect::from_center_half_size(center, extents / 2.0);
110+
111+
// Transform cursor line segment to sprite coordinate system
112+
let world_to_sprite = sprite_transform.affine().inverse();
113+
let cursor_start_sprite =
114+
world_to_sprite.transform_point3(cursor_ray_world.origin);
115+
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);
116+
117+
fn inverse_lerp(a: f32, b: f32, x: f32) -> Option<f32> {
118+
if (b - a).abs() < f32::EPSILON {
119+
None
120+
} else {
121+
Some((x - a) / (b - a))
122+
}
123+
}
124+
125+
// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
126+
// plane in sprite-local space). It may not intersect if, for example, we're
127+
// viewing the sprite side-on
128+
let lerp_factor = inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
129+
let Some(lerp_factor) = lerp_factor else {
130+
// Lerp factor is `None`, meaning the cursor ray is parallel to the sprite
131+
// and misses it
132+
return None;
133+
};
134+
if !(0.0..=1.0).contains(&lerp_factor) {
135+
// Lerp factor is out of range, meaning that while an infinite line cast by
136+
// the cursor would intersect the sprite, the sprite is not between the
137+
// camera's near and far planes
138+
return None;
139+
}
140+
// Otherwise we can interpolate the xy of the start and end positions by the
141+
// lerp factor to get the cursor position in sprite space!
142+
let cursor_pos_sprite = cursor_start_sprite
143+
.lerp(cursor_end_sprite, lerp_factor)
144+
.xy();
145+
146+
let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);
147+
let hit_pos_world =
148+
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
149+
150+
blocked = is_cursor_in_sprite
151+
&& pickable.map(|p| p.should_block_lower) != Some(false);
152+
153+
// Transform point from world to camera space to get the Z distance
154+
let hit_pos_cam = cam_transform
155+
.affine()
156+
.inverse()
157+
.transform_point3(hit_pos_world);
158+
// HitData requires a depth as calculated from the camera's near clipping plane
159+
let depth = -cam_ortho.near - hit_pos_cam.z;
160+
161+
is_cursor_in_sprite.then_some((
162+
entity,
163+
HitData::new(
164+
cam_entity,
165+
depth,
166+
Some(hit_pos_world),
167+
Some(*sprite_transform.back()),
168+
),
169+
))
170+
},
171+
)
172+
.collect();
173+
174+
let order = camera.order as f32;
175+
output.send(PointerHits::new(*pointer, picks, order));
176+
}
177+
}

0 commit comments

Comments
 (0)