bevy/examples/picking/mesh_picking.rs
Aevyrie 54b323ec80
Mesh picking fixes (#16110)
# Objective

- Mesh picking is noisy when a non triangle list is used
- Mesh picking runs even when users don't need it
- Resolve #16065 

## Solution

- Don't add the mesh picking plugin by default
- Remove error spam
2024-10-27 19:03:48 +00:00

255 lines
8.5 KiB
Rust

//! A simple 3D scene to demonstrate mesh picking.
//!
//! [`bevy::picking::backend`] provides an API for adding picking hit tests to any entity. To get
//! started with picking 3d meshes, the [`MeshPickingPlugin`] is provided as a simple starting
//! point, especially useful for debugging. For your game, you may want to use a 3d picking backend
//! provided by your physics engine, or a picking shader, depending on your specific use case.
//!
//! [`bevy::picking`] allows you to compose backends together to make any entity on screen pickable
//! with pointers, regardless of how that entity is rendered. For example, `bevy_ui` and
//! `bevy_sprite` provide their own picking backends that can be enabled at the same time as this
//! mesh picking backend. This makes it painless to deal with cases like the UI or sprites blocking
//! meshes underneath them, or vice versa.
//!
//! If you want to build more complex interactions than afforded by the provided pointer events, you
//! may want to use [`MeshRayCast`] or a full physics engine with raycasting capabilities.
//!
//! By default, the mesh picking plugin will raycast against all entities, which is especially
//! useful for debugging. If you want mesh picking to be opt-in, you can set
//! [`MeshPickingSettings::require_markers`] to `true` and add a [`RayCastPickable`] component to
//! the desired camera and target entities.
use std::f32::consts::PI;
use bevy::{
color::palettes::{
css::{PINK, RED, SILVER},
tailwind::{CYAN_300, YELLOW_300},
},
picking::backend::PointerHits,
prelude::*,
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
// The mesh picking plugin is not enabled by default, because raycasting against all
// meshes has a performance cost.
MeshPickingPlugin,
))
.init_resource::<SceneMaterials>()
.add_systems(Startup, setup)
.add_systems(Update, (on_mesh_hover, rotate))
.run();
}
/// Materials for the scene
#[derive(Resource, Default)]
struct SceneMaterials {
pub white: Handle<StandardMaterial>,
pub ground: Handle<StandardMaterial>,
pub hover: Handle<StandardMaterial>,
pub pressed: Handle<StandardMaterial>,
}
/// A marker component for our shapes so we can query them separately from the ground plane.
#[derive(Component)]
struct Shape;
const SHAPES_X_EXTENT: f32 = 14.0;
const EXTRUSION_X_EXTENT: f32 = 16.0;
const Z_EXTENT: f32 = 5.0;
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut scene_materials: ResMut<SceneMaterials>,
) {
// Set up the materials.
scene_materials.white = materials.add(Color::WHITE);
scene_materials.ground = materials.add(Color::from(SILVER));
scene_materials.hover = materials.add(Color::from(CYAN_300));
scene_materials.pressed = materials.add(Color::from(YELLOW_300));
let shapes = [
meshes.add(Cuboid::default()),
meshes.add(Tetrahedron::default()),
meshes.add(Capsule3d::default()),
meshes.add(Torus::default()),
meshes.add(Cylinder::default()),
meshes.add(Cone::default()),
meshes.add(ConicalFrustum::default()),
meshes.add(Sphere::default().mesh().ico(5).unwrap()),
meshes.add(Sphere::default().mesh().uv(32, 18)),
];
let extrusions = [
meshes.add(Extrusion::new(Rectangle::default(), 1.)),
meshes.add(Extrusion::new(Capsule2d::default(), 1.)),
meshes.add(Extrusion::new(Annulus::default(), 1.)),
meshes.add(Extrusion::new(Circle::default(), 1.)),
meshes.add(Extrusion::new(Ellipse::default(), 1.)),
meshes.add(Extrusion::new(RegularPolygon::default(), 1.)),
meshes.add(Extrusion::new(Triangle2d::default(), 1.)),
];
let num_shapes = shapes.len();
// Spawn the shapes. The meshes will be pickable by default.
for (i, shape) in shapes.into_iter().enumerate() {
commands
.spawn((
Mesh3d(shape),
MeshMaterial3d(scene_materials.white.clone()),
Transform::from_xyz(
-SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT,
2.0,
Z_EXTENT / 2.,
)
.with_rotation(Quat::from_rotation_x(-PI / 4.)),
Shape,
))
.observe(on_pointer_over)
.observe(on_pointer_out)
.observe(on_pointer_down)
.observe(on_pointer_up);
}
let num_extrusions = extrusions.len();
for (i, shape) in extrusions.into_iter().enumerate() {
commands
.spawn((
Mesh3d(shape),
MeshMaterial3d(scene_materials.white.clone()),
Transform::from_xyz(
-EXTRUSION_X_EXTENT / 2.
+ i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT,
2.0,
-Z_EXTENT / 2.,
)
.with_rotation(Quat::from_rotation_x(-PI / 4.)),
Shape,
))
.observe(on_pointer_over)
.observe(on_pointer_out)
.observe(on_pointer_down)
.observe(on_pointer_up);
}
// Disable picking for the ground plane.
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))),
MeshMaterial3d(scene_materials.ground.clone()),
PickingBehavior::IGNORE,
));
// Light
commands.spawn((
PointLight {
shadows_enabled: true,
intensity: 10_000_000.,
range: 100.0,
shadow_depth_bias: 0.2,
..default()
},
Transform::from_xyz(8.0, 16.0, 8.0),
));
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
));
// Instructions
commands.spawn((
Text::new("Hover over the shapes to pick them"),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
/// Changes the material when the pointer is over the mesh.
fn on_pointer_over(
trigger: Trigger<Pointer<Over>>,
scene_materials: Res<SceneMaterials>,
mut query: Query<&mut MeshMaterial3d<StandardMaterial>>,
) {
if let Ok(mut material) = query.get_mut(trigger.entity()) {
material.0 = scene_materials.hover.clone();
}
}
/// Resets the material when the pointer leaves the mesh.
fn on_pointer_out(
trigger: Trigger<Pointer<Out>>,
scene_materials: Res<SceneMaterials>,
mut query: Query<&mut MeshMaterial3d<StandardMaterial>>,
) {
if let Ok(mut material) = query.get_mut(trigger.entity()) {
material.0 = scene_materials.white.clone();
}
}
/// Changes the material when the pointer is pressed.
fn on_pointer_down(
trigger: Trigger<Pointer<Down>>,
scene_materials: Res<SceneMaterials>,
mut query: Query<&mut MeshMaterial3d<StandardMaterial>>,
) {
if let Ok(mut material) = query.get_mut(trigger.entity()) {
material.0 = scene_materials.pressed.clone();
}
}
/// Resets the material when the pointer is released.
fn on_pointer_up(
trigger: Trigger<Pointer<Up>>,
scene_materials: Res<SceneMaterials>,
mut query: Query<&mut MeshMaterial3d<StandardMaterial>>,
) {
if let Ok(mut material) = query.get_mut(trigger.entity()) {
material.0 = scene_materials.hover.clone();
}
}
/// Draws the closest point of intersection for pointer hits.
fn on_mesh_hover(
mut pointer_hits: EventReader<PointerHits>,
meshes: Query<Entity, With<Mesh3d>>,
mut gizmos: Gizmos,
) {
for hit in pointer_hits.read() {
// Get the first mesh hit.
// The hits are sorted by distance from the camera, so this is the closest hit.
let Some(closest_hit) = hit
.picks
.iter()
.filter_map(|(entity, hit)| meshes.get(*entity).map(|_| hit).ok())
.next()
else {
continue;
};
let (Some(point), Some(normal)) = (closest_hit.position, closest_hit.normal) else {
return;
};
gizmos.sphere(point, 0.05, RED);
gizmos.arrow(point, point + normal * 0.5, PINK);
}
}
/// Rotates the shapes.
fn rotate(mut query: Query<&mut Transform, With<Shape>>, time: Res<Time>) {
for mut transform in &mut query {
transform.rotate_y(time.delta_secs() / 2.);
}
}