Skip to content

Commit aaccbe8

Browse files
NthTensoraevyriealice-i-cecile
authored
Upstream CorePlugin from bevy_mod_picking (#13677)
# Objective This is the first of a series of PRs intended to begin the upstreaming process for `bevy_mod_picking`. The purpose of this PR is to: + Create the new `bevy_picking` crate + Upstream `CorePlugin` as `PickingPlugin` + Upstream the core pointer and backend abstractions. This code has been ported verbatim from the corresponding files in [bevy_picking_core](https://github.com/aevyrie/bevy_mod_picking/tree/main/crates/bevy_picking_core/src) with a few tiny naming and docs tweaks. The work here is only an initial foothold to get the up-streaming process started in earnest. We can do refactoring and improvements once this is in-tree. --------- Co-authored-by: Aevyrie <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent eb3c813 commit aaccbe8

File tree

10 files changed

+799
-4
lines changed

10 files changed

+799
-4
lines changed

Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ default = [
6363
"bevy_winit",
6464
"bevy_core_pipeline",
6565
"bevy_pbr",
66+
"bevy_picking",
6667
"bevy_gltf",
6768
"bevy_render",
6869
"bevy_sprite",
@@ -123,6 +124,9 @@ bevy_pbr = [
123124
"bevy_core_pipeline",
124125
]
125126

127+
# Provides picking functionality
128+
bevy_picking = ["bevy_internal/bevy_picking"]
129+
126130
# Provides rendering functionality
127131
bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
128132

crates/bevy_internal/Cargo.toml

+8-4
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
180180
# Provides a collection of developer tools
181181
bevy_dev_tools = ["dep:bevy_dev_tools"]
182182

183+
# Provides a picking functionality
184+
bevy_picking = ["dep:bevy_picking"]
185+
183186
# Enable support for the ios_simulator by downgrading some rendering capabilities
184187
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
185188

@@ -214,18 +217,19 @@ bevy_asset = { path = "../bevy_asset", optional = true, version = "0.14.0-dev" }
214217
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.14.0-dev" }
215218
bevy_color = { path = "../bevy_color", optional = true, version = "0.14.0-dev" }
216219
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.14.0-dev" }
220+
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
221+
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
222+
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
223+
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
217224
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0-dev" }
218225
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.14.0-dev" }
226+
bevy_picking = { path = "../bevy_picking", optional = true, version = "0.14.0-dev" }
219227
bevy_render = { path = "../bevy_render", optional = true, version = "0.14.0-dev" }
220-
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
221228
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.14.0-dev" }
222229
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.14.0-dev" }
223230
bevy_text = { path = "../bevy_text", optional = true, version = "0.14.0-dev" }
224231
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0-dev" }
225232
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0-dev" }
226-
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
227-
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
228-
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
229233

230234
[lints]
231235
workspace = true

crates/bevy_internal/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub use bevy_log as log;
4444
pub use bevy_math as math;
4545
#[cfg(feature = "bevy_pbr")]
4646
pub use bevy_pbr as pbr;
47+
#[cfg(feature = "bevy_picking")]
48+
pub use bevy_picking as picking;
4749
pub use bevy_ptr as ptr;
4850
pub use bevy_reflect as reflect;
4951
#[cfg(feature = "bevy_render")]

crates/bevy_picking/Cargo.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "bevy_picking"
3+
version = "0.14.0-dev"
4+
edition = "2021"
5+
description = "Provides screen picking functionality for Bevy Engine"
6+
homepage = "https://bevyengine.org"
7+
repository = "https://github.com/bevyengine/bevy"
8+
license = "MIT OR Apache-2.0"
9+
10+
[dependencies]
11+
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
12+
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
13+
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
14+
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
15+
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
16+
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
17+
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
18+
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
19+
20+
uuid = { version = "1.1", features = ["v4"] }
21+
22+
[lints]
23+
workspace = true
24+
25+
[package.metadata.docs.rs]
26+
rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"]
27+
all-features = true

crates/bevy_picking/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Bevy Picking

crates/bevy_picking/src/backend.rs

+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
//! This module provides a simple interface for implementing a picking backend.
2+
//!
3+
//! Don't be dissuaded by terminology like "backend"; the idea is dead simple. `bevy_picking`
4+
//! will tell you where pointers are, all you have to do is send an event if the pointers are
5+
//! hitting something. That's it. The rest of this documentation explains the requirements in more
6+
//! detail.
7+
//!
8+
//! Because `bevy_picking` is very loosely coupled with its backends, you can mix and match as
9+
//! many backends as you want. For example, You could use the `rapier` backend to raycast against
10+
//! physics objects, a picking shader backend to pick non-physics meshes, and the `bevy_ui` backend
11+
//! for your UI. The [`PointerHits`]s produced by these various backends will be combined, sorted,
12+
//! and used as a homogeneous input for the picking systems that consume these events.
13+
//!
14+
//! ## Implementation
15+
//!
16+
//! - A picking backend only has one job: read [`PointerLocation`](crate::pointer::PointerLocation)
17+
//! components and produce [`PointerHits`] events. In plain English, a backend is provided the
18+
//! location of pointers, and is asked to provide a list of entities under those pointers.
19+
//!
20+
//! - The [`PointerHits`] events produced by a backend do **not** need to be sorted or filtered, all
21+
//! that is needed is an unordered list of entities and their [`HitData`].
22+
//!
23+
//! - Backends do not need to consider the [`Pickable`](crate::Pickable) component, though they may
24+
//! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy
25+
//! may want to early exit if it intersects an entity that blocks lower entities from being
26+
//! picked.
27+
//!
28+
//! ### Raycasting Backends
29+
//!
30+
//! Backends that require a ray to cast into the scene should use [`ray::RayMap`]. This
31+
//! automatically constructs rays in world space for all cameras and pointers, handling details like
32+
//! viewports and DPI for you.
33+
34+
use bevy_ecs::prelude::*;
35+
use bevy_math::Vec3;
36+
use bevy_reflect::Reflect;
37+
38+
/// Common imports for implementing a picking backend.
39+
pub mod prelude {
40+
pub use super::{ray::RayMap, HitData, PointerHits};
41+
pub use crate::{
42+
pointer::{PointerId, PointerLocation},
43+
PickSet, Pickable,
44+
};
45+
}
46+
47+
/// An event produced by a picking backend after it has run its hit tests, describing the entities
48+
/// under a pointer.
49+
///
50+
/// Some backends may only support providing the topmost entity; this is a valid limitation of some
51+
/// backends. For example, a picking shader might only have data on the topmost rendered output from
52+
/// its buffer.
53+
#[derive(Event, Debug, Clone)]
54+
pub struct PointerHits {
55+
/// The pointer associated with this hit test.
56+
pub pointer: prelude::PointerId,
57+
/// An unordered collection of entities and their distance (depth) from the cursor.
58+
pub picks: Vec<(Entity, HitData)>,
59+
/// Set the order of this group of picks. Normally, this is the
60+
/// [`bevy_render::camera::Camera::order`].
61+
///
62+
/// Used to allow multiple `PointerHits` submitted for the same pointer to be ordered.
63+
/// `PointerHits` with a higher `order` will be checked before those with a lower `order`,
64+
/// regardless of the depth of each entity pick.
65+
///
66+
/// In other words, when pick data is coalesced across all backends, the data is grouped by
67+
/// pointer, then sorted by order, and checked sequentially, sorting each `PointerHits` by
68+
/// entity depth. Events with a higher `order` are effectively on top of events with a lower
69+
/// order.
70+
///
71+
/// ### Why is this an `f32`???
72+
///
73+
/// Bevy UI is special in that it can share a camera with other things being rendered. in order
74+
/// to properly sort them, we need a way to make `bevy_ui`'s order a tiny bit higher, like adding
75+
/// 0.5 to the order. We can't use integers, and we want users to be using camera.order by
76+
/// default, so this is the best solution at the moment.
77+
pub order: f32,
78+
}
79+
80+
impl PointerHits {
81+
#[allow(missing_docs)]
82+
pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self {
83+
Self {
84+
pointer,
85+
picks,
86+
order,
87+
}
88+
}
89+
}
90+
91+
/// Holds data from a successful pointer hit test. See [`HitData::depth`] for important details.
92+
#[derive(Clone, Debug, PartialEq, Reflect)]
93+
pub struct HitData {
94+
/// The camera entity used to detect this hit. Useful when you need to find the ray that was
95+
/// casted for this hit when using a raycasting backend.
96+
pub camera: Entity,
97+
/// `depth` only needs to be self-consistent with other [`PointerHits`]s using the same
98+
/// [`RenderTarget`](bevy_render::camera::RenderTarget). However, it is recommended to use the
99+
/// distance from the pointer to the hit, measured from the near plane of the camera, to the
100+
/// point, in world space.
101+
pub depth: f32,
102+
/// The position of the intersection in the world, if the data is available from the backend.
103+
pub position: Option<Vec3>,
104+
/// The normal vector of the hit test, if the data is available from the backend.
105+
pub normal: Option<Vec3>,
106+
}
107+
108+
impl HitData {
109+
#[allow(missing_docs)]
110+
pub fn new(camera: Entity, depth: f32, position: Option<Vec3>, normal: Option<Vec3>) -> Self {
111+
Self {
112+
camera,
113+
depth,
114+
position,
115+
normal,
116+
}
117+
}
118+
}
119+
120+
pub mod ray {
121+
//! Types and systems for constructing rays from cameras and pointers.
122+
123+
use crate::backend::prelude::{PointerId, PointerLocation};
124+
use bevy_ecs::prelude::*;
125+
use bevy_math::Ray3d;
126+
use bevy_reflect::Reflect;
127+
use bevy_render::camera::Camera;
128+
use bevy_transform::prelude::GlobalTransform;
129+
use bevy_utils::{hashbrown::hash_map::Iter, HashMap};
130+
use bevy_window::PrimaryWindow;
131+
132+
/// Identifies a ray constructed from some (pointer, camera) combination. A pointer can be over
133+
/// multiple cameras, which is why a single pointer may have multiple rays.
134+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Reflect)]
135+
pub struct RayId {
136+
/// The camera whose projection was used to calculate the ray.
137+
pub camera: Entity,
138+
/// The pointer whose pixel coordinates were used to calculate the ray.
139+
pub pointer: PointerId,
140+
}
141+
142+
impl RayId {
143+
/// Construct a [`RayId`].
144+
pub fn new(camera: Entity, pointer: PointerId) -> Self {
145+
Self { camera, pointer }
146+
}
147+
}
148+
149+
/// A map from [`RayId`] to [`Ray3d`].
150+
///
151+
/// This map is cleared and re-populated every frame before any backends run. Ray-based picking
152+
/// backends should use this when possible, as it automatically handles viewports, DPI, and
153+
/// other details of building rays from pointer locations.
154+
///
155+
/// ## Usage
156+
///
157+
/// Iterate over each [`Ray3d`] and its [`RayId`] with [`RayMap::iter`].
158+
///
159+
/// ```
160+
/// # use bevy_ecs::prelude::*;
161+
/// # use bevy_picking::backend::ray::RayMap;
162+
/// # use bevy_picking::backend::PointerHits;
163+
/// // My raycasting backend
164+
/// pub fn update_hits(ray_map: Res<RayMap>, mut output_events: EventWriter<PointerHits>,) {
165+
/// for (&ray_id, &ray) in ray_map.iter() {
166+
/// // Run a raycast with each ray, returning any `PointerHits` found.
167+
/// }
168+
/// }
169+
/// ```
170+
#[derive(Clone, Debug, Default, Resource)]
171+
pub struct RayMap {
172+
map: HashMap<RayId, Ray3d>,
173+
}
174+
175+
impl RayMap {
176+
/// Iterates over all world space rays for every picking pointer.
177+
pub fn iter(&self) -> Iter<'_, RayId, Ray3d> {
178+
self.map.iter()
179+
}
180+
181+
/// The hash map of all rays cast in the current frame.
182+
pub fn map(&self) -> &HashMap<RayId, Ray3d> {
183+
&self.map
184+
}
185+
186+
/// Clears the [`RayMap`] and re-populates it with one ray for each
187+
/// combination of pointer entity and camera entity where the pointer
188+
/// intersects the camera's viewport.
189+
pub fn repopulate(
190+
mut ray_map: ResMut<Self>,
191+
primary_window_entity: Query<Entity, With<PrimaryWindow>>,
192+
cameras: Query<(Entity, &Camera, &GlobalTransform)>,
193+
pointers: Query<(&PointerId, &PointerLocation)>,
194+
) {
195+
ray_map.map.clear();
196+
197+
for (camera_entity, camera, camera_tfm) in &cameras {
198+
if !camera.is_active {
199+
continue;
200+
}
201+
202+
for (&pointer_id, pointer_loc) in &pointers {
203+
if let Some(ray) =
204+
make_ray(&primary_window_entity, camera, camera_tfm, pointer_loc)
205+
{
206+
ray_map
207+
.map
208+
.insert(RayId::new(camera_entity, pointer_id), ray);
209+
}
210+
}
211+
}
212+
}
213+
}
214+
215+
fn make_ray(
216+
primary_window_entity: &Query<Entity, With<PrimaryWindow>>,
217+
camera: &Camera,
218+
camera_tfm: &GlobalTransform,
219+
pointer_loc: &PointerLocation,
220+
) -> Option<Ray3d> {
221+
let pointer_loc = pointer_loc.location()?;
222+
if !pointer_loc.is_in_viewport(camera, primary_window_entity) {
223+
return None;
224+
}
225+
let mut viewport_pos = pointer_loc.position;
226+
if let Some(viewport) = &camera.viewport {
227+
let viewport_logical = camera.to_logical(viewport.physical_position)?;
228+
viewport_pos -= viewport_logical;
229+
}
230+
camera.viewport_to_world(camera_tfm, viewport_pos)
231+
}
232+
}

0 commit comments

Comments
 (0)