Files
Martin Edlund c3c118c20c Add support for custom picking data (#23245)
# Objective

- Adds the ability for picking backends to add custom hit data
- Fixes #16186 

## Solution

- Adds an `extra` field to the HitData struct which can take any Data
that implements HitDataExtra. This is stored in an Arc which can be
downcast with a helper function in the system that consumes the hit.
- In the original ticket I suggested using a generic, however this
caused extensive code changes and complications down the line with the
HoverMap and OverMap so I decided against it.

## Testing

- I added an example custom_hit_data to test the new feature
- I tested all other picking examples to ensure they still work as
expected
- I tested the examples on Ubuntu
- I additionally tested the custom_hit_data example in wasm 

---

## Showcase

<details>
  <summary>Click to view showcase</summary>

### Creating custom hits:
```rust
let picks: Vec<(Entity, HitData)> = ray_cast
            .cast_ray(ray, &settings)
            .iter()
            .map(|(entity, hit)| {
                let extra = TriangleHitInfo {
                    triangle_vertices: hit.triangle,
                };

                let hit_data = HitData::new_with_extra(
                    ray_id.camera,
                    hit.distance,
                    Some(hit.point),
                    Some(hit.normal),
                    extra,
                );

                (*entity, hit_data)
            })
            .collect();
```

### Reading custom hits:
```rust
   for hits in pointer_hits.read() {
        for (_, hit) in &hits.picks {
            let Some(info) = hit.extra_as::<TriangleHitInfo>() else {
                continue;
            };
            let Some(vertices) = info.triangle_vertices else {
                continue;
            };

            // do something cool with your custom hit data
        }
    }
```

### An example of what you can do with this
<img width="1291" height="730" alt="image"
src="https://github.com/user-attachments/assets/2d0e8aed-7059-470a-a0f6-1453356cfe6a"
/>


</details>
2026-04-23 16:18:18 +00:00

206 lines
6.3 KiB
Rust

//! Demonstrates a custom picking backend with custom hit data.
//!
//! The example contains pickable 3D meshes. When a mesh is hovered, a custom
//! picking backend performs a ray cast against the mesh and retrieves the
//! triangle that was hit. The triangle vertices are stored in a custom struct
//! (`TriangleHitInfo`) that implements `HitDataExtra`, and saved into `HitData`
//! structs. This information is not available by default in `HitData` and thus
//! requires its `extra` field. A follow-up system reads the hit data and draws
//! an outline around the hovered triangle using gizmos.
use bevy::{
color::palettes::css::*,
picking::{
backend::{ray::RayMap, HitData, PointerHits},
mesh_picking::{
ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility},
MeshPickingSettings,
},
prelude::*,
PickingSettings, PickingSystems,
},
prelude::*,
};
fn main() {
App::new()
.add_plugins((DefaultPlugins, MeshPickingPlugin))
.insert_resource(MeshPickingSettings {
require_markers: true,
..default()
})
.insert_resource(PickingSettings {
is_window_picking_enabled: false,
..default()
})
.init_resource::<HoveredTriangles>()
.add_systems(Startup, (setup_gizmos, setup_scene))
.add_systems(
PreUpdate,
(
custom_backend_system.in_set(PickingSystems::Backend),
cache_hovered_triangles.after(PickingSystems::Backend),
),
)
.add_systems(Update, draw_hit_gizmos)
.run();
}
/// The custom hit data used by our picking backend. All structs that implement
/// `Send + Sync + fmt::Debug + 'static` automatically implement `HitDataExtra`
/// and can be used as extra data in `HitData`.
#[derive(Debug)]
struct TriangleHitInfo {
triangle_vertices: Option<[Vec3; 3]>,
}
#[derive(Resource, Default)]
struct HoveredTriangles(Vec<TriangleOverlay>);
struct TriangleOverlay {
position: Vec3,
normal: Vec3,
vertices: [Vec3; 3],
}
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let shapes: [(Mesh, Color); 3] = [
(Cuboid::default().into(), RED.into()),
(Sphere::default().mesh().ico(2).unwrap(), GREEN.into()),
(Cylinder::default().into(), BLUE.into()),
];
for (i, (mesh, color)) in shapes.iter().enumerate() {
let x = i as f32 * 1.5 - 1.5;
let material = materials.add(StandardMaterial::from_color(*color));
commands.spawn((
Mesh3d(meshes.add(mesh.clone())),
MeshMaterial3d(material),
Transform::from_xyz(x, 0.5, 0.0),
Pickable::default(),
));
}
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(30.0, 30.0))),
MeshMaterial3d(materials.add(Color::from(DARK_GRAY))),
Pickable::IGNORE,
));
commands.spawn((PointLight::default(), Transform::from_xyz(0.0, 8.0, 4.0)));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 2.5, 6.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
));
}
fn setup_gizmos(mut config_store: ResMut<GizmoConfigStore>) {
let (config, _) = config_store.config_mut::<DefaultGizmoConfigGroup>();
config.depth_bias = -1.0;
config.line.width = 3.0;
}
fn custom_backend_system(
ray_map: Res<RayMap>,
cameras: Query<&Camera>,
pickables: Query<&Pickable>,
mut ray_cast: MeshRayCast,
mut pointer_hits: MessageWriter<PointerHits>,
) {
for (&ray_id, &ray) in ray_map.iter() {
let Ok(camera) = cameras.get(ray_id.camera) else {
continue;
};
let settings = MeshRayCastSettings {
visibility: RayCastVisibility::VisibleInView,
filter: &|e| pickables.get(e).is_ok_and(|p| p.is_hoverable),
early_exit_test: &|entity_hit| {
pickables
.get(entity_hit)
.is_ok_and(|p| p.should_block_lower)
},
};
let picks: Vec<(Entity, HitData)> = ray_cast
.cast_ray(ray, &settings)
.iter()
.map(|(entity, hit)| {
let extra = TriangleHitInfo {
triangle_vertices: hit.triangle,
};
let hit_data = HitData::new_with_extra(
ray_id.camera,
hit.distance,
Some(hit.point),
Some(hit.normal),
extra,
);
(*entity, hit_data)
})
.collect();
if !picks.is_empty() {
pointer_hits.write(PointerHits::new(ray_id.pointer, picks, camera.order as f32));
}
}
}
fn cache_hovered_triangles(
mut pointer_hits: MessageReader<PointerHits>,
mut hovered_triangles: ResMut<HoveredTriangles>,
) {
hovered_triangles.0.clear();
for hits in pointer_hits.read() {
for (_, hit) in &hits.picks {
let (Some(position), Some(normal)) = (hit.position, hit.normal) else {
continue;
};
let Some(info) = hit.extra_as::<TriangleHitInfo>() else {
continue;
};
let Some(vertices) = info.triangle_vertices else {
continue;
};
hovered_triangles.0.push(TriangleOverlay {
position,
normal,
vertices,
});
}
}
}
fn draw_hit_gizmos(hovered_triangles: Res<HoveredTriangles>, mut gizmos: Gizmos) {
for triangle in &hovered_triangles.0 {
gizmos.arrow(
triangle.position,
triangle.position + triangle.normal.normalize() * 0.5,
WHITE,
);
let vertices = triangle.vertices;
let center = (vertices[0] + vertices[1] + vertices[2]) / 3.0;
let offset = triangle.normal.normalize_or_zero() * 0.025;
// The outline is made bigger and offset a bit to prevent being covered
// by the mesh
let outline = vertices.map(|vertex| center + (vertex - center) * 1.05 + offset);
gizmos.line(outline[0], outline[1], WHITE);
gizmos.line(outline[1], outline[2], WHITE);
gizmos.line(outline[2], outline[0], WHITE);
}
}