Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mesh picking backend and MeshRayCast system parameter #15800

Merged
merged 33 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
281638e
Initial port
Jondolf Oct 1, 2024
9b73b44
Rename to bevy_picking_mesh and add to default plugins
Jondolf Oct 2, 2024
85360b5
Merge branch 'main' into mesh-picking
Jondolf Oct 2, 2024
6ccc539
Clean up and rename "raycast" to "ray cast" everywhere
Jondolf Oct 3, 2024
74cda85
Clean up, refactor module structure, and rename types
Jondolf Oct 8, 2024
171f032
Add basic mesh picking example
Jondolf Oct 8, 2024
efe26d9
Merge branch 'main' into mesh-picking
Jondolf Oct 8, 2024
4b728dc
Move to `bevy_picking` crate
Jondolf Oct 8, 2024
7b4a2e7
Fix doc things and name
Jondolf Oct 8, 2024
3526cfa
Clean up and improve docs
Jondolf Oct 9, 2024
3af0565
Fix scaling issue and clean up
Jondolf Oct 9, 2024
0ce79a5
Add back backface marker component `RayCastBackfaces`
Jondolf Oct 9, 2024
3d544c6
Add benchmark
Jondolf Oct 9, 2024
673d75f
Improve example
Jondolf Oct 9, 2024
4495be8
Make Aevyrie a co-author :)
Jondolf Oct 9, 2024
96b3a5f
Fix missing examples in docs
Jondolf Oct 9, 2024
8ded91a
Merge branch 'main' into mesh-picking
Jondolf Oct 9, 2024
9c0b4c6
Huh, this exists too... Fix missing feature in docs
Jondolf Oct 9, 2024
eb744fb
Fix example
Jondolf Oct 9, 2024
e7fca35
Add sick bouncing laser example by Aevyrie
Jondolf Oct 10, 2024
7ae5c7b
Fix imports in doc examples
Jondolf Oct 10, 2024
a9f4b79
Fix bench
Jondolf Oct 10, 2024
0c865f3
I keep forgetting this :/
Jondolf Oct 10, 2024
cdb3520
Make the `mesh_ray_cast` example clearer
Jondolf Oct 10, 2024
dcfdd01
Merge branch 'main' into mesh-picking
Jondolf Oct 10, 2024
402d3c9
Fix bench for real for real
Jondolf Oct 10, 2024
0909366
Use `chunks_exact` and change attribute panics to errors
Jondolf Oct 11, 2024
8c51e67
Yeet `IntoUsize` and use slice instead of vec
Jondolf Oct 11, 2024
390465f
Improve docs
Jondolf Oct 11, 2024
86a2ea4
Make hit data docs more consistent
Jondolf Oct 11, 2024
4e504b3
Optimize ray-AABB intersection
Jondolf Oct 11, 2024
7aaa6f2
Merge branch 'main' into mesh-picking
Jondolf Oct 11, 2024
379b710
Merge branch 'main' into mesh-picking
mockersf Oct 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ default = [
"bevy_gilrs",
"bevy_gizmos",
"bevy_gltf",
"bevy_mesh_picking_backend",
"bevy_pbr",
"bevy_picking",
"bevy_remote",
Expand All @@ -136,6 +137,9 @@ default = [
"x11",
]

# Provides an implementation for picking meshes
bevy_mesh_picking_backend = ["bevy_picking"]

# Provides an implementation for picking sprites
bevy_sprite_picking_backend = ["bevy_picking"]

Expand Down Expand Up @@ -1213,6 +1217,17 @@ setup = [
],
]

[[example]]
name = "mesh_ray_cast"
path = "examples/3d/mesh_ray_cast.rs"
doc-scrape-examples = true

[package.metadata.example.mesh_ray_cast]
name = "Mesh Ray Cast"
description = "Demonstrates ray casting with the `MeshRayCast` system parameter"
category = "3D Rendering"
wasm = true

[[example]]
name = "lightmaps"
path = "examples/3d/lightmaps.rs"
Expand Down Expand Up @@ -3695,6 +3710,18 @@ description = "Demonstrates how to rotate the skybox and the environment map sim
category = "3D Rendering"
wasm = false

[[example]]
name = "mesh_picking"
path = "examples/picking/mesh_picking.rs"
doc-scrape-examples = true
required-features = ["bevy_mesh_picking_backend"]

[package.metadata.example.mesh_picking]
name = "Mesh Picking"
description = "Demonstrates picking meshes"
category = "Picking"
wasm = true

[[example]]
name = "simple_picking"
path = "examples/picking/simple_picking.rs"
Expand Down
6 changes: 6 additions & 0 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ bevy_app = { path = "../crates/bevy_app" }
bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] }
bevy_hierarchy = { path = "../crates/bevy_hierarchy" }
bevy_math = { path = "../crates/bevy_math" }
bevy_picking = { path = "../crates/bevy_picking", features = ["bevy_mesh"] }
bevy_reflect = { path = "../crates/bevy_reflect", features = ["functions"] }
bevy_render = { path = "../crates/bevy_render" }
bevy_tasks = { path = "../crates/bevy_tasks" }
Expand All @@ -37,6 +38,11 @@ name = "ecs"
path = "benches/bevy_ecs/benches.rs"
harness = false

[[bench]]
name = "ray_mesh_intersection"
path = "benches/bevy_picking/ray_mesh_intersection.rs"
harness = false

[[bench]]
name = "reflect_function"
path = "benches/bevy_reflect/function.rs"
Expand Down
120 changes: 120 additions & 0 deletions benches/benches/bevy_picking/ray_mesh_intersection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use bevy_math::{Dir3, Mat4, Ray3d, Vec3};
use bevy_picking::{mesh_picking::ray_cast, prelude::*};
use criterion::{black_box, criterion_group, criterion_main, Criterion};

fn ptoxznorm(p: u32, size: u32) -> (f32, f32) {
let ij = (p / (size), p % (size));
(ij.0 as f32 / size as f32, ij.1 as f32 / size as f32)
}

struct SimpleMesh {
positions: Vec<[f32; 3]>,
normals: Vec<[f32; 3]>,
indices: Vec<u32>,
}

fn mesh_creation(vertices_per_side: u32) -> SimpleMesh {
let mut positions = Vec::new();
let mut normals = Vec::new();
for p in 0..vertices_per_side.pow(2) {
let xz = ptoxznorm(p, vertices_per_side);
positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]);
normals.push([0.0, 1.0, 0.0]);
}

let mut indices = vec![];
for p in 0..vertices_per_side.pow(2) {
if p % (vertices_per_side) != vertices_per_side - 1
&& p / (vertices_per_side) != vertices_per_side - 1
{
indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]);
indices.extend_from_slice(&[p + vertices_per_side, p + 1, p + vertices_per_side + 1]);
}
}

SimpleMesh {
positions,
normals,
indices,
}
}

fn ray_mesh_intersection(c: &mut Criterion) {
let mut group = c.benchmark_group("ray_mesh_intersection");
group.warm_up_time(std::time::Duration::from_millis(500));

for vertices_per_side in [10_u32, 100, 1000] {
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
let mesh_to_world = Mat4::IDENTITY;
let mesh = mesh_creation(vertices_per_side);

b.iter(|| {
black_box(ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
ray_cast::Backfaces::Cull,
));
});
});
}
}

fn ray_mesh_intersection_no_cull(c: &mut Criterion) {
let mut group = c.benchmark_group("ray_mesh_intersection_no_cull");
group.warm_up_time(std::time::Duration::from_millis(500));

for vertices_per_side in [10_u32, 100, 1000] {
group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| {
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y);
let mesh_to_world = Mat4::IDENTITY;
let mesh = mesh_creation(vertices_per_side);

b.iter(|| {
black_box(ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
ray_cast::Backfaces::Include,
));
});
});
}
}

fn ray_mesh_intersection_no_intersection(c: &mut Criterion) {
let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection");
group.warm_up_time(std::time::Duration::from_millis(500));

for vertices_per_side in [10_u32, 100, 1000] {
group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| {
let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X);
let mesh_to_world = Mat4::IDENTITY;
let mesh = mesh_creation(vertices_per_side);

b.iter(|| {
black_box(ray_cast::ray_mesh_intersection(
ray,
&mesh_to_world,
&mesh.positions,
Some(&mesh.normals),
Some(&mesh.indices),
ray_cast::Backfaces::Cull,
));
});
});
}
}

criterion_group!(
benches,
ray_mesh_intersection,
ray_mesh_intersection_no_cull,
ray_mesh_intersection_no_intersection
);
criterion_main!(benches);
3 changes: 2 additions & 1 deletion crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,10 @@ bevy_dev_tools = ["dep:bevy_dev_tools"]
# Enable support for the Bevy Remote Protocol
bevy_remote = ["dep:bevy_remote"]

# Provides a picking functionality
# Provides picking functionality
bevy_picking = [
"dep:bevy_picking",
"bevy_picking/bevy_mesh",
"bevy_ui?/bevy_picking",
"bevy_sprite?/bevy_picking",
]
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_picking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ homepage = "https://bevyengine.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"

[features]
# Provides a mesh picking backend
bevy_mesh = ["dep:bevy_mesh", "dep:crossbeam-channel"]

[dependencies]
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
Expand All @@ -15,13 +19,15 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.15.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
bevy_mesh = { path = "../bevy_mesh", version = "0.15.0-dev", optional = true }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }

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

[lints]
Expand Down
10 changes: 10 additions & 0 deletions crates/bevy_picking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ pub mod backend;
pub mod events;
pub mod focus;
pub mod input;
#[cfg(feature = "bevy_mesh")]
pub mod mesh_picking;
pub mod pointer;

use bevy_app::prelude::*;
Expand All @@ -166,6 +168,12 @@ use bevy_reflect::prelude::*;
///
/// This includes the most common types in this crate, re-exported for your convenience.
pub mod prelude {
#[cfg(feature = "bevy_mesh")]
#[doc(hidden)]
pub use crate::mesh_picking::{
ray_cast::{MeshRayCast, RayCastBackfaces, RayCastSettings, RayCastVisibility},
MeshPickingBackend, MeshPickingBackendSettings, RayCastPickable,
};
#[doc(hidden)]
pub use crate::{
events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins,
Expand Down Expand Up @@ -274,6 +282,8 @@ impl Plugin for DefaultPickingPlugins {
PickingPlugin::default(),
InteractionPlugin,
));
#[cfg(feature = "bevy_mesh")]
app.add_plugins(mesh_picking::MeshPickingBackend);
}
}

Expand Down
133 changes: 133 additions & 0 deletions crates/bevy_picking/src/mesh_picking/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! A [mesh ray casting](ray_cast) backend for [`bevy_picking`](crate).
//!
//! By default, all meshes are pickable. Picking can be disabled for individual entities
//! by adding [`PickingBehavior::IGNORE`].
//!
//! To make mesh picking entirely opt-in, set [`MeshPickingBackendSettings::require_markers`]
//! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities.
//!
//! To manually perform mesh ray casts independent of picking, use the [`MeshRayCast`] system parameter.

pub mod ray_cast;

use crate::{
backend::{ray::RayMap, HitData, PointerHits},
prelude::*,
PickSet,
};
use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_reflect::prelude::*;
use bevy_render::{prelude::*, view::RenderLayers};
use ray_cast::{MeshRayCast, RayCastSettings, RayCastVisibility, SimplifiedMesh};

/// Runtime settings for the [`MeshPickingBackend`].
#[derive(Resource, Reflect)]
#[reflect(Resource, Default)]
pub struct MeshPickingBackendSettings {
/// When set to `true` ray casting will only happen between cameras and entities marked with
/// [`RayCastPickable`]. `false` by default.
///
/// This setting is provided to give you fine-grained control over which cameras and entities
/// should be used by the mesh picking backend at runtime.
pub require_markers: bool,

/// Determines how mesh picking should consider [`Visibility`]. When set to [`RayCastVisibility::Any`],
/// ray casts can be performed against both visible and hidden entities.
///
/// Defaults to [`RayCastVisibility::VisibleInView`], only performing picking against visible entities
/// that are in the view of a camera.
pub ray_cast_visibility: RayCastVisibility,
}

impl Default for MeshPickingBackendSettings {
fn default() -> Self {
Self {
require_markers: false,
ray_cast_visibility: RayCastVisibility::VisibleInView,
}
}
}

/// An optional component that marks cameras and target entities that should be used in the [`MeshPickingBackend`].
/// Only needed if [`MeshPickingBackendSettings::require_markers`] is set to `true`, and ignored otherwise.
#[derive(Debug, Clone, Default, Component, Reflect)]
#[reflect(Component, Default)]
pub struct RayCastPickable;

/// Adds the mesh picking backend to your app.
#[derive(Clone, Default)]
pub struct MeshPickingBackend;

impl Plugin for MeshPickingBackend {
fn build(&self, app: &mut App) {
app.init_resource::<MeshPickingBackendSettings>()
.register_type::<(RayCastPickable, MeshPickingBackendSettings, SimplifiedMesh)>()
.add_systems(PreUpdate, update_hits.in_set(PickSet::Backend));
}
}

/// Casts rays into the scene using [`MeshPickingBackendSettings`] and sends [`PointerHits`] events.
#[allow(clippy::too_many_arguments)]
pub fn update_hits(
backend_settings: Res<MeshPickingBackendSettings>,
ray_map: Res<RayMap>,
picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>,
pickables: Query<&PickingBehavior>,
marked_targets: Query<&RayCastPickable>,
layers: Query<&RenderLayers>,
mut ray_cast: MeshRayCast,
mut output: EventWriter<PointerHits>,
) {
for (&ray_id, &ray) in ray_map.map().iter() {
let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
continue;
Jondolf marked this conversation as resolved.
Show resolved Hide resolved
};
if backend_settings.require_markers && cam_pickable.is_none() {
continue;
}

let cam_layers = cam_layers.to_owned().unwrap_or_default();

let settings = RayCastSettings {
visibility: backend_settings.ray_cast_visibility,
filter: &|entity| {
let marker_requirement =
!backend_settings.require_markers || marked_targets.get(entity).is_ok();

// Other entities missing render layers are on the default layer 0
let entity_layers = layers.get(entity).cloned().unwrap_or_default();
let render_layers_match = cam_layers.intersects(&entity_layers);

let is_pickable = pickables
.get(entity)
.map(|p| p.is_hoverable)
.unwrap_or(true);

marker_requirement && render_layers_match && is_pickable
},
early_exit_test: &|entity_hit| {
pickables
.get(entity_hit)
.is_ok_and(|pickable| pickable.should_block_lower)
},
};
let picks = ray_cast
.cast_ray(ray, &settings)
.iter()
.map(|(entity, hit)| {
let hit_data = HitData::new(
ray_id.camera,
hit.distance,
Some(hit.point),
Some(hit.normal),
);
(*entity, hit_data)
})
.collect::<Vec<_>>();
let order = camera.order as f32;
if !picks.is_empty() {
output.send(PointerHits::new(ray_id.pointer, picks, order));
}
}
}
Loading