Skip to main content

custom_hit_data/
custom_hit_data.rs

1//! Demonstrates a custom picking backend with custom hit data.
2//!
3//! The example contains pickable 3D meshes. When a mesh is hovered, a custom
4//! picking backend performs a ray cast against the mesh and retrieves the
5//! triangle that was hit. The triangle vertices are stored in a custom struct
6//! (`TriangleHitInfo`) that implements `HitDataExtra`, and saved into `HitData`
7//! structs. This information is not available by default in `HitData` and thus
8//! requires its `extra` field. A follow-up system reads the hit data and draws
9//! an outline around the hovered triangle using gizmos.
10
11use bevy::{
12    color::palettes::css::*,
13    picking::{
14        backend::{ray::RayMap, HitData, PointerHits},
15        mesh_picking::{
16            ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility},
17            MeshPickingSettings,
18        },
19        prelude::*,
20        PickingSettings, PickingSystems,
21    },
22    prelude::*,
23};
24
25fn main() {
26    App::new()
27        .add_plugins((DefaultPlugins, MeshPickingPlugin))
28        .insert_resource(MeshPickingSettings {
29            require_markers: true,
30            ..default()
31        })
32        .insert_resource(PickingSettings {
33            is_window_picking_enabled: false,
34            ..default()
35        })
36        .init_resource::<HoveredTriangles>()
37        .add_systems(Startup, (setup_gizmos, setup_scene))
38        .add_systems(
39            PreUpdate,
40            (
41                custom_backend_system.in_set(PickingSystems::Backend),
42                cache_hovered_triangles.after(PickingSystems::Backend),
43            ),
44        )
45        .add_systems(Update, draw_hit_gizmos)
46        .run();
47}
48
49/// The custom hit data used by our picking backend. All structs that implement
50/// `Send + Sync + fmt::Debug + 'static` automatically implement `HitDataExtra`
51/// and can be used as extra data in `HitData`.
52#[derive(Debug)]
53struct TriangleHitInfo {
54    triangle_vertices: Option<[Vec3; 3]>,
55}
56
57#[derive(Resource, Default)]
58struct HoveredTriangles(Vec<TriangleOverlay>);
59
60struct TriangleOverlay {
61    position: Vec3,
62    normal: Vec3,
63    vertices: [Vec3; 3],
64}
65
66fn setup_scene(
67    mut commands: Commands,
68    mut meshes: ResMut<Assets<Mesh>>,
69    mut materials: ResMut<Assets<StandardMaterial>>,
70) {
71    let shapes: [(Mesh, Color); 3] = [
72        (Cuboid::default().into(), RED.into()),
73        (Sphere::default().mesh().ico(2).unwrap(), GREEN.into()),
74        (Cylinder::default().into(), BLUE.into()),
75    ];
76
77    for (i, (mesh, color)) in shapes.iter().enumerate() {
78        let x = i as f32 * 1.5 - 1.5;
79        let material = materials.add(StandardMaterial::from_color(*color));
80
81        commands.spawn((
82            Mesh3d(meshes.add(mesh.clone())),
83            MeshMaterial3d(material),
84            Transform::from_xyz(x, 0.5, 0.0),
85            Pickable::default(),
86        ));
87    }
88
89    commands.spawn((
90        Mesh3d(meshes.add(Plane3d::default().mesh().size(30.0, 30.0))),
91        MeshMaterial3d(materials.add(Color::from(DARK_GRAY))),
92        Pickable::IGNORE,
93    ));
94
95    commands.spawn((PointLight::default(), Transform::from_xyz(0.0, 8.0, 4.0)));
96
97    commands.spawn((
98        Camera3d::default(),
99        Transform::from_xyz(0.0, 2.5, 6.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
100    ));
101}
102
103fn setup_gizmos(mut config_store: ResMut<GizmoConfigStore>) {
104    let (config, _) = config_store.config_mut::<DefaultGizmoConfigGroup>();
105    config.depth_bias = -1.0;
106    config.line.width = 3.0;
107}
108
109fn custom_backend_system(
110    ray_map: Res<RayMap>,
111    cameras: Query<&Camera>,
112    pickables: Query<&Pickable>,
113    mut ray_cast: MeshRayCast,
114    mut pointer_hits: MessageWriter<PointerHits>,
115) {
116    for (&ray_id, &ray) in ray_map.iter() {
117        let Ok(camera) = cameras.get(ray_id.camera) else {
118            continue;
119        };
120
121        let settings = MeshRayCastSettings {
122            visibility: RayCastVisibility::VisibleInView,
123            filter: &|e| pickables.get(e).is_ok_and(|p| p.is_hoverable),
124            early_exit_test: &|entity_hit| {
125                pickables
126                    .get(entity_hit)
127                    .is_ok_and(|p| p.should_block_lower)
128            },
129        };
130
131        let picks: Vec<(Entity, HitData)> = ray_cast
132            .cast_ray(ray, &settings)
133            .iter()
134            .map(|(entity, hit)| {
135                let extra = TriangleHitInfo {
136                    triangle_vertices: hit.triangle,
137                };
138
139                let hit_data = HitData::new_with_extra(
140                    ray_id.camera,
141                    hit.distance,
142                    Some(hit.point),
143                    Some(hit.normal),
144                    extra,
145                );
146
147                (*entity, hit_data)
148            })
149            .collect();
150
151        if !picks.is_empty() {
152            pointer_hits.write(PointerHits::new(ray_id.pointer, picks, camera.order as f32));
153        }
154    }
155}
156
157fn cache_hovered_triangles(
158    mut pointer_hits: MessageReader<PointerHits>,
159    mut hovered_triangles: ResMut<HoveredTriangles>,
160) {
161    hovered_triangles.0.clear();
162
163    for hits in pointer_hits.read() {
164        for (_, hit) in &hits.picks {
165            let (Some(position), Some(normal)) = (hit.position, hit.normal) else {
166                continue;
167            };
168
169            let Some(info) = hit.extra_as::<TriangleHitInfo>() else {
170                continue;
171            };
172            let Some(vertices) = info.triangle_vertices else {
173                continue;
174            };
175
176            hovered_triangles.0.push(TriangleOverlay {
177                position,
178                normal,
179                vertices,
180            });
181        }
182    }
183}
184
185fn draw_hit_gizmos(hovered_triangles: Res<HoveredTriangles>, mut gizmos: Gizmos) {
186    for triangle in &hovered_triangles.0 {
187        gizmos.arrow(
188            triangle.position,
189            triangle.position + triangle.normal.normalize() * 0.5,
190            WHITE,
191        );
192
193        let vertices = triangle.vertices;
194        let center = (vertices[0] + vertices[1] + vertices[2]) / 3.0;
195        let offset = triangle.normal.normalize_or_zero() * 0.025;
196
197        // The outline is made bigger and offset a bit to prevent being covered
198        // by the mesh
199        let outline = vertices.map(|vertex| center + (vertex - center) * 1.05 + offset);
200
201        gizmos.line(outline[0], outline[1], WHITE);
202        gizmos.line(outline[1], outline[2], WHITE);
203        gizmos.line(outline[2], outline[0], WHITE);
204    }
205}