mod bounding;
mod debug;
mod highlight;
mod interactable;
mod raycast;
mod select;
pub use crate::{
debug::DebugPickingPlugin,
highlight::{HighlightablePickMesh, PickHighlightParams},
interactable::{HoverEvents, InteractableMesh, InteractablePickingPlugin, MouseDownEvents},
select::SelectablePickMesh,
};
use crate::bounding::*;
use crate::raycast::*;
use bevy::{
prelude::*,
render::{
camera::Camera,
mesh::{Indices, Mesh, VertexAttributeValues},
pipeline::PrimitiveTopology,
},
tasks::prelude::*,
window::{CursorMoved, WindowId},
};
use core::convert::TryInto;
use std::collections::HashMap;
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
fn build(&self, app: &mut AppBuilder) {
app.init_resource::<PickState>()
.init_resource::<PickHighlightParams>()
.add_system(build_bound_sphere.system())
.add_system(build_rays.system())
.add_system(pick_mesh.system());
}
}
pub struct PickState {
ray_map: HashMap<Group, Ray3d>,
ordered_pick_list_map: HashMap<Group, Vec<(Entity, Intersection)>>,
pub enabled: bool,
pub last_cursor_pos: Vec2,
}
impl PickState {
pub fn list(&self, group: Group) -> Option<&Vec<(Entity, Intersection)>> {
self.ordered_pick_list_map.get(&group)
}
pub fn top(&self, group: Group) -> Option<&(Entity, Intersection)> {
match self.ordered_pick_list_map.get(&group) {
Some(list) => list.first(),
None => None,
}
}
pub fn top_all(&self) -> Option<Vec<(&Group, &Entity, &Intersection)>> {
let mut result = Vec::new();
for (group, picklist) in self.ordered_pick_list_map.iter() {
if let Some(pick) = picklist.first() {
let (entity, intersection) = pick;
result.push((group, entity, intersection));
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn empty_pick_list(&mut self) {
for (_group, intersection_list) in self.ordered_pick_list_map.iter_mut() {
intersection_list.clear();
}
}
}
impl Default for PickState {
fn default() -> Self {
PickState {
ray_map: HashMap::new(),
ordered_pick_list_map: HashMap::new(),
enabled: true,
last_cursor_pos: Vec2::zero(),
}
}
}
#[derive(Debug, PartialOrd, PartialEq, Copy, Clone)]
pub struct Intersection {
normal: Ray3d,
pick_distance: f32,
triangle: Triangle,
}
impl Intersection {
fn new(normal: Ray3d, pick_distance: f32, triangle: Triangle) -> Self {
Intersection {
normal,
pick_distance,
triangle,
}
}
pub fn position(&self) -> &Vec3 {
self.normal.origin()
}
pub fn normal(&self) -> &Vec3 {
self.normal.direction()
}
pub fn distance(&self) -> f32 {
self.pick_distance
}
pub fn world_triangle(&self) -> Triangle {
self.triangle
}
}
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
pub struct Group(pub u8);
impl core::ops::Deref for Group {
type Target = u8;
fn deref(&'_ self) -> &'_ Self::Target {
&self.0
}
}
impl Default for Group {
fn default() -> Self {
Group(0)
}
}
#[derive(Debug)]
pub struct PickableMesh {
groups: Vec<Group>,
intersections: HashMap<Group, Option<Intersection>>,
bounding_sphere: BoundVol,
}
impl PickableMesh {
pub fn new(groups: Vec<Group>) -> Self {
let mut picks: HashMap<Group, Option<Intersection>> = HashMap::new();
for group in &groups {
picks.insert(*group, None);
}
PickableMesh {
groups,
intersections: picks,
bounding_sphere: BoundVol::None,
}
}
pub fn intersection(&self, group: &Group) -> Result<&Option<Intersection>, String> {
self.intersections
.get(group)
.ok_or(format!("PickableMesh does not belong to group {}", **group))
}
pub fn with_bounding_sphere(&self, mesh: Handle<Mesh>) -> Self {
PickableMesh {
groups: self.groups.clone(),
intersections: self.intersections.clone(),
bounding_sphere: BoundVol::Loading(mesh),
}
}
}
impl Default for PickableMesh {
fn default() -> Self {
let mut picks = HashMap::new();
picks.insert(Group::default(), None);
PickableMesh {
groups: [Group::default()].into(),
bounding_sphere: BoundVol::None,
intersections: picks,
}
}
}
#[derive(Debug)]
pub enum PickMethod {
CameraCursor(WindowId, UpdatePicks),
CameraScreenSpace(Vec2),
Transform,
}
#[derive(Debug, Clone, Copy)]
pub enum UpdatePicks {
Always,
OnMouseEvent,
}
pub struct PickSource {
pub groups: Option<Vec<Group>>,
pub pick_method: PickMethod,
pub cursor_events: EventReader<CursorMoved>,
}
impl PickSource {
pub fn new(group: Vec<Group>, pick_method: PickMethod) -> Self {
PickSource {
groups: Some(group),
pick_method,
..Default::default()
}
}
pub fn with_group(self, new_group: Group) -> Self {
let new_groups = match self.groups {
Some(group) => {
let mut new_groups = group;
new_groups.push(new_group);
Some(new_groups)
}
None => Some(vec![new_group]),
};
PickSource {
groups: new_groups,
..self
}
}
pub fn with_pick_method(mut self, pick_method: PickMethod) -> Self {
self.pick_method = pick_method;
self
}
}
impl Default for PickSource {
fn default() -> Self {
PickSource {
groups: Some(vec![Group::default()]),
pick_method: PickMethod::CameraCursor(WindowId::primary(), UpdatePicks::Always),
cursor_events: EventReader::default(),
}
}
}
fn build_rays(
mut pick_state: ResMut<PickState>,
cursor: Res<Events<CursorMoved>>,
windows: Res<Windows>,
mut pick_source_query: Query<(&mut PickSource, &Transform, Option<&Camera>)>,
) {
pick_state.ray_map.clear();
if !pick_state.enabled {
return;
}
for (mut pick_source, transform, camera) in &mut pick_source_query.iter_mut() {
let group_numbers = match &pick_source.groups {
Some(groups) => groups.clone(),
None => continue,
};
match pick_source.pick_method {
PickMethod::CameraCursor(window_id, update_picks) => {
let projection_matrix = match camera {
Some(camera) => camera.projection_matrix,
None => panic!("The PickingSource in group(s) {:?} has a {:?} but no associated Camera component", group_numbers, pick_source.pick_method),
};
let cursor_pos_screen: Vec2 = match pick_source.cursor_events.latest(&cursor) {
Some(cursor_moved) => {
if cursor_moved.id == window_id {
cursor_moved.position
} else {
continue;
}
}
None => match update_picks {
UpdatePicks::Always => pick_state.last_cursor_pos,
UpdatePicks::OnMouseEvent => {
continue;
}
},
};
pick_state.last_cursor_pos = cursor_pos_screen;
let window = windows.get(window_id).unwrap();
let screen_size = Vec2::from([window.width() as f32, window.height() as f32]);
let cursor_pos_ndc: Vec3 =
((cursor_pos_screen / screen_size) * 2.0 - Vec2::from([1.0, 1.0])).extend(1.0);
let camera_matrix = transform.compute_matrix();
let (_, _, camera_position) = camera_matrix.to_scale_rotation_translation();
let ndc_to_world: Mat4 = camera_matrix * projection_matrix.inverse();
let cursor_position: Vec3 = ndc_to_world.transform_point3(cursor_pos_ndc);
let ray_direction = cursor_position - camera_position;
let pick_ray = Ray3d::new(camera_position, ray_direction);
for group in group_numbers {
if pick_state.ray_map.insert(group, pick_ray).is_some() {
panic!(
"Multiple PickingSources have been added to pick group: {:?}",
group
);
}
}
}
PickMethod::CameraScreenSpace(coordinates_ndc) => {
let projection_matrix = match camera {
Some(camera) => camera.projection_matrix,
None => panic!("The PickingSource in group(s) {:?} has a {:?} but no associated Camera component", group_numbers, pick_source.pick_method),
};
let cursor_pos_ndc: Vec3 = coordinates_ndc.extend(1.0);
let camera_matrix = transform.compute_matrix();
let (_, _, camera_position) = camera_matrix.to_scale_rotation_translation();
let ndc_to_world: Mat4 = camera_matrix * projection_matrix.inverse();
let cursor_position: Vec3 = ndc_to_world.transform_point3(cursor_pos_ndc);
let ray_direction = cursor_position - camera_position;
let pick_ray = Ray3d::new(camera_position, ray_direction);
for group in group_numbers {
if pick_state.ray_map.insert(group, pick_ray).is_some() {
panic!(
"Multiple PickingSources have been added to pick group: {:?}",
group
);
}
}
}
PickMethod::Transform => {
let pick_position_ndc = Vec3::from([0.0, 0.0, 1.0]);
let source_transform = transform.compute_matrix();
let pick_position = source_transform.transform_point3(pick_position_ndc);
let (_, _, source_origin) = source_transform.to_scale_rotation_translation();
let ray_direction = pick_position - source_origin;
let pick_ray = Ray3d::new(source_origin, ray_direction);
for group in group_numbers {
if pick_state.ray_map.insert(group, pick_ray).is_some() {
panic!(
"Multiple PickingSources have been added to pick group: {:?}",
group
);
}
}
}
}
}
}
fn pick_mesh(
mut pick_state: ResMut<PickState>,
mut meshes: ResMut<Assets<Mesh>>,
_pool: Res<ComputeTaskPool>,
mut mesh_query: Query<(
&Handle<Mesh>,
&GlobalTransform,
&mut PickableMesh,
Entity,
&Visible,
)>,
) {
let ray_cull = info_span!("ray culling");
let raycast = info_span!("raycast");
if !pick_state.enabled {
pick_state.empty_pick_list();
return;
}
if pick_state.ray_map.is_empty() {
return;
} else {
pick_state.empty_pick_list();
}
for (mesh_handle, transform, mut pickable, entity, draw) in &mut mesh_query.iter_mut() {
if !draw.is_visible {
continue;
}
let pick_rays: Vec<(Group, Ray3d)> = {
let _ray_cull_guard = ray_cull.enter();
pickable
.groups
.iter()
.filter_map(|group| {
if let Some(ray) = pick_state.ray_map.get(group) {
if let BoundVol::Loaded(sphere) = &pickable.bounding_sphere {
let scaled_radius =
1.01 * sphere.radius() * transform.scale.max_element();
let translated_origin =
sphere.origin() * transform.scale + transform.translation;
let det = (ray.direction().dot(*ray.origin() - translated_origin))
.powi(2)
- (Vec3::length_squared(*ray.origin() - translated_origin)
- scaled_radius.powi(2));
if det >= 0.0 {
Some((*group, *ray))
} else {
None
}
} else {
Some((*group, *ray))
}
} else {
None
}
})
.collect()
};
if pick_rays.is_empty() {
continue;
}
if let Some(mesh) = meshes.get_mut(mesh_handle) {
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
panic!("bevy_mod_picking only supports TriangleList topology");
}
let _raycast_guard = raycast.enter();
let vertex_positions: Vec<[f32; 3]> = match mesh.attribute(Mesh::ATTRIBUTE_POSITION) {
None => panic!("Mesh does not contain vertex positions"),
Some(vertex_values) => match &vertex_values {
VertexAttributeValues::Float3(positions) => positions.clone(),
_ => panic!("Unexpected vertex types in ATTRIBUTE_POSITION"),
},
};
if let Some(indices) = &mesh.indices() {
for (pick_group, pick_ray) in pick_rays {
let mesh_to_world = transform.compute_matrix();
let new_intersection = match indices {
Indices::U16(vector) => ray_mesh_intersection(
&mesh_to_world,
&vertex_positions,
&pick_ray,
vector,
),
Indices::U32(vector) => ray_mesh_intersection(
&mesh_to_world,
&vertex_positions,
&pick_ray,
vector,
),
};
if let Some(intersection) = new_intersection {
match pick_state.ordered_pick_list_map.get_mut(&pick_group) {
Some(list) => list.push((entity, intersection)),
None => {
pick_state
.ordered_pick_list_map
.insert(pick_group, Vec::from([(entity, intersection)]));
}
}
}
pickable.intersections.insert(pick_group, new_intersection);
}
} else {
panic!(
"No index matrix found in mesh {:?}\n{:?}",
mesh_handle, mesh
);
}
}
}
for (_group, intersection_list) in pick_state.ordered_pick_list_map.iter_mut() {
intersection_list.sort_by(|a, b| {
a.1.pick_distance
.partial_cmp(&b.1.pick_distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
}
fn ray_mesh_intersection<T: TryInto<usize> + Copy>(
mesh_to_world: &Mat4,
vertex_positions: &[[f32; 3]],
pick_ray: &Ray3d,
indices: &[T],
) -> Option<Intersection> {
let mut min_pick_distance = f32::MAX;
let mut pick_intersection: Option<Intersection> = None;
if indices.len() % 3 == 0 {
for index in indices.chunks(3) {
let mut world_vertices: [Vec3; 3] = [Vec3::zero(), Vec3::zero(), Vec3::zero()];
for i in 0..3 {
if let Ok(vertex_index) = index[i].try_into() {
world_vertices[i] =
mesh_to_world.transform_point3(Vec3::from(vertex_positions[vertex_index]));
} else {
panic!("Failed to convert index into usize.");
}
}
let world_triangle = Triangle::from(world_vertices);
if let Some(intersection) =
ray_triangle_intersection(pick_ray, &world_triangle, RaycastAlgorithm::default())
{
let distance: f32 = (*intersection.origin() - *pick_ray.origin()).length().abs();
if distance < min_pick_distance {
min_pick_distance = distance;
pick_intersection =
Some(Intersection::new(intersection, distance, world_triangle));
}
}
}
}
pick_intersection
}