Skip to content

Commit 0e30b68

Browse files
Jondolfaevyrietbillingtonmockersf
authored
Add mesh picking backend and MeshRayCast system parameter (#15800)
# Objective Closes #15545. `bevy_picking` supports UI and sprite picking, but not mesh picking. Being able to pick meshes would be extremely useful for various games, tools, and our own examples, as well as scene editors and inspectors. So, we need a mesh picking backend! Luckily, [`bevy_mod_picking`](https://github.com/aevyrie/bevy_mod_picking) (which `bevy_picking` is based on) by @aevyrie already has a [backend for it](https://github.com/aevyrie/bevy_mod_picking/blob/74f0c3c0fbc8048632ba46fd8f14e26aaea9c76c/backends/bevy_picking_raycast/src/lib.rs) using [`bevy_mod_raycast`](https://github.com/aevyrie/bevy_mod_raycast). As a side product of adding mesh picking, we also get support for performing ray casts on meshes! ## Solution Upstream a large chunk of the immediate-mode ray casting functionality from `bevy_mod_raycast`, and add a mesh picking backend based on `bevy_mod_picking`. Huge thanks to @aevyrie who did all the hard work on these incredible crates! All meshes are pickable by default. Picking can be disabled for individual entities by adding `PickingBehavior::IGNORE`, like normal. Or, if you want mesh picking to be entirely opt-in, you can set `MeshPickingBackendSettings::require_markers` to `true` and add a `RayCastPickable` component to the desired camera and target entities. You can also use the new `MeshRayCast` system parameter to cast rays into the world manually: ```rust fn ray_cast_system(mut ray_cast: MeshRayCast, foo_query: Query<(), With<Foo>>) { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); // Only ray cast against entities with the `Foo` component. let filter = |entity| foo_query.contains(entity); // Never early-exit. Note that you can change behavior per-entity. let early_exit_test = |_entity| false; // Ignore the visibility of entities. This allows ray casting hidden entities. let visibility = RayCastVisibility::Any; let settings = RayCastSettings::default() .with_filter(&filter) .with_early_exit_test(&early_exit_test) .with_visibility(visibility); // Cast the ray with the settings, returning a list of intersections. let hits = ray_cast.cast_ray(ray, &settings); } ``` This is largely a direct port, but I did make several changes to match our APIs better, remove things we don't need or that I think are unnecessary, and do some general improvements to code quality and documentation. ### Changes Relative to `bevy_mod_raycast` and `bevy_mod_picking` - Every `Raycast` and "raycast" has been renamed to `RayCast` and "ray cast" (similar reasoning as the "Naming" section in #15724) - `Raycast` system param has been renamed to `MeshRayCast` to avoid naming conflicts and to be explicit that it is not for colliders - `RaycastBackend` has been renamed to `MeshPickingBackend` - `RayCastVisibility` variants are now `Any`, `Visible`, and `VisibleInView` instead of `Ignore`, `MustBeVisible`, and `MustBeVisibleAndInView` - `NoBackfaceCulling` has been renamed to `RayCastBackfaces`, to avoid implying that it affects the rendering of backfaces for meshes (it doesn't) - `SimplifiedMesh` and `RayCastBackfaces` live near other ray casting API types, not in their own 10 LoC module - All intersection logic and types are in the same `intersections` module, not split across several modules - Some intersection types have been renamed to be clearer and more consistent - `IntersectionData` -> `RayMeshHit` - `RayHit` -> `RayTriangleHit` - General documentation and code quality improvements ### Removed / Not Ported - Removed unused ray helpers and types, like `PrimitiveIntersection` - Removed getters on intersection types, and made their properties public - There is no `2d` feature, and `Raycast::mesh_query` and `Raycast::mesh2d_query` have been merged into `MeshRayCast::mesh_query`, which handles both 2D and 3D - I assume this existed previously because `Mesh2dHandle` used to be in `bevy_sprite`. Now both the 2D and 3D mesh are in `bevy_render`. - There is no `debug` feature or ray debug rendering - There is no deferred API (`RaycastSource`) - There is no `CursorRayPlugin` (the picking backend handles this) ### Note for Reviewers In case it's helpful, the [first commit](281638e) here is essentially a one-to-one port. The rest of the commits are primarily refactoring and cleaning things up in the ways listed earlier, as well as changes to the module structure. It may also be useful to compare the original [picking backend](https://github.com/aevyrie/bevy_mod_picking/blob/74f0c3c0fbc8048632ba46fd8f14e26aaea9c76c/backends/bevy_picking_raycast/src/lib.rs) and [`bevy_mod_raycast`](https://github.com/aevyrie/bevy_mod_raycast) to this PR. Feel free to mention if there are any changes that I should revert or something I should not include in this PR. ## Testing I tested mesh picking and relevant components in some examples, for both 2D and 3D meshes, and added a new `mesh_picking` example. I also ~~stole~~ ported over the [ray-mesh intersection benchmark](https://github.com/aevyrie/bevy_mod_raycast/blob/dbc5ef32fe48997a1a7eeec7434d9dd8b829e52e/benches/ray_mesh_intersection.rs) from `bevy_mod_raycast`. --- ## Showcase Below is a version of the `2d_shapes` example modified to demonstrate 2D mesh picking. This is not included in this PR. https://github.com/user-attachments/assets/7742528c-8630-4c00-bacd-81576ac432bf And below is the new `mesh_picking` example: https://github.com/user-attachments/assets/b65c7a5a-fa3a-4c2d-8bbd-e7a2c772986e There is also a really cool new `mesh_ray_cast` example ported over from `bevy_mod_raycast`: https://github.com/user-attachments/assets/3c5eb6c0-bd94-4fb0-bec6-8a85668a06c9 --------- Co-authored-by: Aevyrie <[email protected]> Co-authored-by: Trent <[email protected]> Co-authored-by: François Mockers <[email protected]>
1 parent 6f7d0e5 commit 0e30b68

File tree

13 files changed

+1350
-1
lines changed

13 files changed

+1350
-1
lines changed

Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ default = [
111111
"bevy_gilrs",
112112
"bevy_gizmos",
113113
"bevy_gltf",
114+
"bevy_mesh_picking_backend",
114115
"bevy_pbr",
115116
"bevy_picking",
116117
"bevy_remote",
@@ -136,6 +137,9 @@ default = [
136137
"x11",
137138
]
138139

140+
# Provides an implementation for picking meshes
141+
bevy_mesh_picking_backend = ["bevy_picking"]
142+
139143
# Provides an implementation for picking sprites
140144
bevy_sprite_picking_backend = ["bevy_picking"]
141145

@@ -1213,6 +1217,17 @@ setup = [
12131217
],
12141218
]
12151219

1220+
[[example]]
1221+
name = "mesh_ray_cast"
1222+
path = "examples/3d/mesh_ray_cast.rs"
1223+
doc-scrape-examples = true
1224+
1225+
[package.metadata.example.mesh_ray_cast]
1226+
name = "Mesh Ray Cast"
1227+
description = "Demonstrates ray casting with the `MeshRayCast` system parameter"
1228+
category = "3D Rendering"
1229+
wasm = true
1230+
12161231
[[example]]
12171232
name = "lightmaps"
12181233
path = "examples/3d/lightmaps.rs"
@@ -3695,6 +3710,18 @@ description = "Demonstrates how to rotate the skybox and the environment map sim
36953710
category = "3D Rendering"
36963711
wasm = false
36973712

3713+
[[example]]
3714+
name = "mesh_picking"
3715+
path = "examples/picking/mesh_picking.rs"
3716+
doc-scrape-examples = true
3717+
required-features = ["bevy_mesh_picking_backend"]
3718+
3719+
[package.metadata.example.mesh_picking]
3720+
name = "Mesh Picking"
3721+
description = "Demonstrates picking meshes"
3722+
category = "Picking"
3723+
wasm = true
3724+
36983725
[[example]]
36993726
name = "simple_picking"
37003727
path = "examples/picking/simple_picking.rs"

benches/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ bevy_app = { path = "../crates/bevy_app" }
1414
bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] }
1515
bevy_hierarchy = { path = "../crates/bevy_hierarchy" }
1616
bevy_math = { path = "../crates/bevy_math" }
17+
bevy_picking = { path = "../crates/bevy_picking", features = ["bevy_mesh"] }
1718
bevy_reflect = { path = "../crates/bevy_reflect", features = ["functions"] }
1819
bevy_render = { path = "../crates/bevy_render" }
1920
bevy_tasks = { path = "../crates/bevy_tasks" }
@@ -37,6 +38,11 @@ name = "ecs"
3738
path = "benches/bevy_ecs/benches.rs"
3839
harness = false
3940

41+
[[bench]]
42+
name = "ray_mesh_intersection"
43+
path = "benches/bevy_picking/ray_mesh_intersection.rs"
44+
harness = false
45+
4046
[[bench]]
4147
name = "reflect_function"
4248
path = "benches/bevy_reflect/function.rs"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use bevy_math::{Dir3, Mat4, Ray3d, Vec3};
2+
use bevy_picking::{mesh_picking::ray_cast, prelude::*};
3+
use criterion::{black_box, criterion_group, criterion_main, Criterion};
4+
5+
fn ptoxznorm(p: u32, size: u32) -> (f32, f32) {
6+
let ij = (p / (size), p % (size));
7+
(ij.0 as f32 / size as f32, ij.1 as f32 / size as f32)
8+
}
9+
10+
struct SimpleMesh {
11+
positions: Vec<[f32; 3]>,
12+
normals: Vec<[f32; 3]>,
13+
indices: Vec<u32>,
14+
}
15+
16+
fn mesh_creation(vertices_per_side: u32) -> SimpleMesh {
17+
let mut positions = Vec::new();
18+
let mut normals = Vec::new();
19+
for p in 0..vertices_per_side.pow(2) {
20+
let xz = ptoxznorm(p, vertices_per_side);
21+
positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]);
22+
normals.push([0.0, 1.0, 0.0]);
23+
}
24+
25+
let mut indices = vec![];
26+
for p in 0..vertices_per_side.pow(2) {
27+
if p % (vertices_per_side) != vertices_per_side - 1
28+
&& p / (vertices_per_side) != vertices_per_side - 1
29+
{
30+
indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]);
31+
indices.extend_from_slice(&[p + vertices_per_side, p + 1, p + vertices_per_side + 1]);
32+
}
33+
}
34+
35+
SimpleMesh {
36+
positions,
37+
normals,
38+
indices,
39+
}
40+
}
41+
42+
fn ray_mesh_intersection(c: &mut Criterion) {
43+
let mut group = c.benchmark_group("ray_mesh_intersection");
44+
group.warm_up_time(std::time::Duration::from_millis(500));
45+
46+
for vertices_per_side in [10_u32, 100, 1000] {
47+
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
48+
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
49+
let mesh_to_world = Mat4::IDENTITY;
50+
let mesh = mesh_creation(vertices_per_side);
51+
52+
b.iter(|| {
53+
black_box(ray_cast::ray_mesh_intersection(
54+
ray,
55+
&mesh_to_world,
56+
&mesh.positions,
57+
Some(&mesh.normals),
58+
Some(&mesh.indices),
59+
ray_cast::Backfaces::Cull,
60+
));
61+
});
62+
});
63+
}
64+
}
65+
66+
fn ray_mesh_intersection_no_cull(c: &mut Criterion) {
67+
let mut group = c.benchmark_group("ray_mesh_intersection_no_cull");
68+
group.warm_up_time(std::time::Duration::from_millis(500));
69+
70+
for vertices_per_side in [10_u32, 100, 1000] {
71+
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
72+
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
73+
let mesh_to_world = Mat4::IDENTITY;
74+
let mesh = mesh_creation(vertices_per_side);
75+
76+
b.iter(|| {
77+
black_box(ray_cast::ray_mesh_intersection(
78+
ray,
79+
&mesh_to_world,
80+
&mesh.positions,
81+
Some(&mesh.normals),
82+
Some(&mesh.indices),
83+
ray_cast::Backfaces::Include,
84+
));
85+
});
86+
});
87+
}
88+
}
89+
90+
fn ray_mesh_intersection_no_intersection(c: &mut Criterion) {
91+
let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection");
92+
group.warm_up_time(std::time::Duration::from_millis(500));
93+
94+
for vertices_per_side in [10_u32, 100, 1000] {
95+
group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| {
96+
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X);
97+
let mesh_to_world = Mat4::IDENTITY;
98+
let mesh = mesh_creation(vertices_per_side);
99+
100+
b.iter(|| {
101+
black_box(ray_cast::ray_mesh_intersection(
102+
ray,
103+
&mesh_to_world,
104+
&mesh.positions,
105+
Some(&mesh.normals),
106+
Some(&mesh.indices),
107+
ray_cast::Backfaces::Cull,
108+
));
109+
});
110+
});
111+
}
112+
}
113+
114+
criterion_group!(
115+
benches,
116+
ray_mesh_intersection,
117+
ray_mesh_intersection_no_cull,
118+
ray_mesh_intersection_no_intersection
119+
);
120+
criterion_main!(benches);

crates/bevy_internal/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,10 @@ bevy_dev_tools = ["dep:bevy_dev_tools"]
214214
# Enable support for the Bevy Remote Protocol
215215
bevy_remote = ["dep:bevy_remote"]
216216

217-
# Provides a picking functionality
217+
# Provides picking functionality
218218
bevy_picking = [
219219
"dep:bevy_picking",
220+
"bevy_picking/bevy_mesh",
220221
"bevy_ui?/bevy_picking",
221222
"bevy_sprite?/bevy_picking",
222223
]

crates/bevy_picking/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ homepage = "https://bevyengine.org"
77
repository = "https://github.com/bevyengine/bevy"
88
license = "MIT OR Apache-2.0"
99

10+
[features]
11+
# Provides a mesh picking backend
12+
bevy_mesh = ["dep:bevy_mesh", "dep:crossbeam-channel"]
13+
1014
[dependencies]
1115
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
1216
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
@@ -15,13 +19,15 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
1519
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
1620
bevy_input = { path = "../bevy_input", version = "0.15.0-dev" }
1721
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
22+
bevy_mesh = { path = "../bevy_mesh", version = "0.15.0-dev", optional = true }
1823
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
1924
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
2025
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
2126
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
2227
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
2328
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
2429

30+
crossbeam-channel = { version = "0.5", optional = true }
2531
uuid = { version = "1.1", features = ["v4"] }
2632

2733
[lints]

crates/bevy_picking/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ pub mod backend;
156156
pub mod events;
157157
pub mod focus;
158158
pub mod input;
159+
#[cfg(feature = "bevy_mesh")]
160+
pub mod mesh_picking;
159161
pub mod pointer;
160162

161163
use bevy_app::prelude::*;
@@ -166,6 +168,12 @@ use bevy_reflect::prelude::*;
166168
///
167169
/// This includes the most common types in this crate, re-exported for your convenience.
168170
pub mod prelude {
171+
#[cfg(feature = "bevy_mesh")]
172+
#[doc(hidden)]
173+
pub use crate::mesh_picking::{
174+
ray_cast::{MeshRayCast, RayCastBackfaces, RayCastSettings, RayCastVisibility},
175+
MeshPickingBackend, MeshPickingBackendSettings, RayCastPickable,
176+
};
169177
#[doc(hidden)]
170178
pub use crate::{
171179
events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins,
@@ -274,6 +282,8 @@ impl Plugin for DefaultPickingPlugins {
274282
PickingPlugin::default(),
275283
InteractionPlugin,
276284
));
285+
#[cfg(feature = "bevy_mesh")]
286+
app.add_plugins(mesh_picking::MeshPickingBackend);
277287
}
278288
}
279289

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//! A [mesh ray casting](ray_cast) backend for [`bevy_picking`](crate).
2+
//!
3+
//! By default, all meshes are pickable. Picking can be disabled for individual entities
4+
//! by adding [`PickingBehavior::IGNORE`].
5+
//!
6+
//! To make mesh picking entirely opt-in, set [`MeshPickingBackendSettings::require_markers`]
7+
//! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities.
8+
//!
9+
//! To manually perform mesh ray casts independent of picking, use the [`MeshRayCast`] system parameter.
10+
11+
pub mod ray_cast;
12+
13+
use crate::{
14+
backend::{ray::RayMap, HitData, PointerHits},
15+
prelude::*,
16+
PickSet,
17+
};
18+
use bevy_app::prelude::*;
19+
use bevy_ecs::prelude::*;
20+
use bevy_reflect::prelude::*;
21+
use bevy_render::{prelude::*, view::RenderLayers};
22+
use ray_cast::{MeshRayCast, RayCastSettings, RayCastVisibility, SimplifiedMesh};
23+
24+
/// Runtime settings for the [`MeshPickingBackend`].
25+
#[derive(Resource, Reflect)]
26+
#[reflect(Resource, Default)]
27+
pub struct MeshPickingBackendSettings {
28+
/// When set to `true` ray casting will only happen between cameras and entities marked with
29+
/// [`RayCastPickable`]. `false` by default.
30+
///
31+
/// This setting is provided to give you fine-grained control over which cameras and entities
32+
/// should be used by the mesh picking backend at runtime.
33+
pub require_markers: bool,
34+
35+
/// Determines how mesh picking should consider [`Visibility`]. When set to [`RayCastVisibility::Any`],
36+
/// ray casts can be performed against both visible and hidden entities.
37+
///
38+
/// Defaults to [`RayCastVisibility::VisibleInView`], only performing picking against visible entities
39+
/// that are in the view of a camera.
40+
pub ray_cast_visibility: RayCastVisibility,
41+
}
42+
43+
impl Default for MeshPickingBackendSettings {
44+
fn default() -> Self {
45+
Self {
46+
require_markers: false,
47+
ray_cast_visibility: RayCastVisibility::VisibleInView,
48+
}
49+
}
50+
}
51+
52+
/// An optional component that marks cameras and target entities that should be used in the [`MeshPickingBackend`].
53+
/// Only needed if [`MeshPickingBackendSettings::require_markers`] is set to `true`, and ignored otherwise.
54+
#[derive(Debug, Clone, Default, Component, Reflect)]
55+
#[reflect(Component, Default)]
56+
pub struct RayCastPickable;
57+
58+
/// Adds the mesh picking backend to your app.
59+
#[derive(Clone, Default)]
60+
pub struct MeshPickingBackend;
61+
62+
impl Plugin for MeshPickingBackend {
63+
fn build(&self, app: &mut App) {
64+
app.init_resource::<MeshPickingBackendSettings>()
65+
.register_type::<(RayCastPickable, MeshPickingBackendSettings, SimplifiedMesh)>()
66+
.add_systems(PreUpdate, update_hits.in_set(PickSet::Backend));
67+
}
68+
}
69+
70+
/// Casts rays into the scene using [`MeshPickingBackendSettings`] and sends [`PointerHits`] events.
71+
#[allow(clippy::too_many_arguments)]
72+
pub fn update_hits(
73+
backend_settings: Res<MeshPickingBackendSettings>,
74+
ray_map: Res<RayMap>,
75+
picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>,
76+
pickables: Query<&PickingBehavior>,
77+
marked_targets: Query<&RayCastPickable>,
78+
layers: Query<&RenderLayers>,
79+
mut ray_cast: MeshRayCast,
80+
mut output: EventWriter<PointerHits>,
81+
) {
82+
for (&ray_id, &ray) in ray_map.map().iter() {
83+
let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
84+
continue;
85+
};
86+
if backend_settings.require_markers && cam_pickable.is_none() {
87+
continue;
88+
}
89+
90+
let cam_layers = cam_layers.to_owned().unwrap_or_default();
91+
92+
let settings = RayCastSettings {
93+
visibility: backend_settings.ray_cast_visibility,
94+
filter: &|entity| {
95+
let marker_requirement =
96+
!backend_settings.require_markers || marked_targets.get(entity).is_ok();
97+
98+
// Other entities missing render layers are on the default layer 0
99+
let entity_layers = layers.get(entity).cloned().unwrap_or_default();
100+
let render_layers_match = cam_layers.intersects(&entity_layers);
101+
102+
let is_pickable = pickables
103+
.get(entity)
104+
.map(|p| p.is_hoverable)
105+
.unwrap_or(true);
106+
107+
marker_requirement && render_layers_match && is_pickable
108+
},
109+
early_exit_test: &|entity_hit| {
110+
pickables
111+
.get(entity_hit)
112+
.is_ok_and(|pickable| pickable.should_block_lower)
113+
},
114+
};
115+
let picks = ray_cast
116+
.cast_ray(ray, &settings)
117+
.iter()
118+
.map(|(entity, hit)| {
119+
let hit_data = HitData::new(
120+
ray_id.camera,
121+
hit.distance,
122+
Some(hit.point),
123+
Some(hit.normal),
124+
);
125+
(*entity, hit_data)
126+
})
127+
.collect::<Vec<_>>();
128+
let order = camera.order as f32;
129+
if !picks.is_empty() {
130+
output.send(PointerHits::new(ray_id.pointer, picks, order));
131+
}
132+
}
133+
}

0 commit comments

Comments
 (0)