Skip to content

Commit

Permalink
Add the RayMap resource and use it in rapier and mod_raycast backen…
Browse files Browse the repository at this point in the history
…ds (#289)

* add a RayMap resource

This map will hold a mapping from (camera entity, pointer id) to Ray. This will
be created before any backends run. It will be available for use by backends and
picking event handlers.

* use the RayMap in the rapier backend

* use RayMap in the bevy_mod_raycast backend

* add changelog entries

* use RenderLayers::default() instead of all()

* Fix changelog

* Move ray module into backend module

* add window scale test example

---------

Co-authored-by: Aevyrie <[email protected]>
  • Loading branch information
bonsairobo and aevyrie authored Feb 23, 2024
1 parent 7aafa82 commit f8e0484
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 137 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# UNRELEASED

- Fixed: Ray construction now respects DPI / window scale
- Added: `RayMap` resource that contains a `Ray` for every (camera, pointer) pair
- Changed: rapier and bevy_mod_raycast backends use the `RayMap` instead of constructing their own
rays
- Fixed: rapier and bevy_mod_raycast backends use `RenderLayers::default` when a camera is missing
them
- Added: support for `bevy_ui` `UiScale`.
- Fixed: the bevy ui backend now ignores clipped areas of UI nodes.
- Added: `RaycastBackendSettings::raycast_visibility` to support picking hidden meshes.
Expand All @@ -21,8 +27,8 @@
- Faster compile times.
- Sprites now support atlases, scale, rotation, and anchors.
- All `egui` widgets, including side panels, are now supported.
- `bevy_mod_raycast` and `bevy_rapier` backends are now even simpler, no longer requiring any
marker components to function.
- `bevy_mod_raycast` and `bevy_rapier` backends are now even simpler, no longer requiring any marker
components to function.
- More flexible picking behavior and `bevy_ui` compatibility with the updated `Pickable` component.
- Better support for cameras settings such as `is_active`, `RenderLayers`, and `show_ui`.

Expand Down
108 changes: 43 additions & 65 deletions backends/bevy_picking_rapier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{prelude::*, view::RenderLayers};
use bevy_transform::prelude::*;
use bevy_window::PrimaryWindow;

use bevy_picking_core::backend::prelude::*;
use bevy_rapier3d::prelude::*;
Expand Down Expand Up @@ -74,82 +72,62 @@ pub struct RapierPickable;
/// Raycasts into the scene using [`RapierBackendSettings`] and [`PointerLocation`]s, then outputs
/// [`PointerHits`].
pub fn update_hits(
pointers: Query<(&PointerId, &PointerLocation)>,
primary_window_entity: Query<Entity, With<PrimaryWindow>>,
picking_cameras: Query<(
Entity,
&Camera,
&GlobalTransform,
Option<&RapierPickable>,
Option<&RenderLayers>,
)>,
backend_settings: Res<RapierBackendSettings>,
ray_map: Res<RayMap>,
picking_cameras: Query<(&Camera, Option<&RapierPickable>, Option<&RenderLayers>)>,
pickables: Query<&Pickable>,
marked_targets: Query<&RapierPickable>,
layers: Query<&RenderLayers>,
backend_settings: Res<RapierBackendSettings>,
rapier_context: Option<Res<RapierContext>>,
mut output_events: EventWriter<PointerHits>,
) {
let Some(rapier_context) = rapier_context else {
return;
};

for (pointer_id, pointer_location) in &pointers {
let pointer_location = match pointer_location.location() {
Some(l) => l,
None => continue,
for (&ray_id, &ray) in ray_map.map().iter() {
let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
continue;
};
for (cam_entity, camera, ray, cam_layers) in picking_cameras
.iter()
.filter(|(_, camera, ..)| {
camera.is_active && pointer_location.is_in_viewport(camera, &primary_window_entity)
})
.filter(|(.., marker, _)| marker.is_some() || !backend_settings.require_markers)
.filter_map(|(entity, camera, transform, _, layers)| {
let mut viewport_pos = pointer_location.position;
if let Some(viewport) = &camera.viewport {
viewport_pos -= viewport.physical_position.as_vec2();
}
camera
.viewport_to_world(transform, viewport_pos)
.map(|ray| (entity, camera, ray, layers))
})
{
if let Some((entity, hit_data)) = rapier_context
.cast_ray_and_get_normal(
ray.origin,
ray.direction,
f32::MAX,
true,
QueryFilter::new().predicate(&|entity| {
let marker_requirement =
!backend_settings.require_markers || marked_targets.get(entity).is_ok();
if backend_settings.require_markers && cam_pickable.is_none() {
continue;
}

// Cameras missing render layers intersect all layers
let cam_layers = cam_layers.copied().unwrap_or(RenderLayers::all());
// Other entities missing render layers are on the default layer 0
let entity_layers = layers.get(entity).copied().unwrap_or_default();
let render_layers_match = cam_layers.intersects(&entity_layers);
let cam_layers = cam_layers.copied().unwrap_or_default();

let pickable = pickables
.get(entity)
.map(|p| *p != Pickable::IGNORE)
.unwrap_or(true);
marker_requirement && render_layers_match && pickable
}),
)
.map(|(entity, hit)| {
let hit_data =
HitData::new(cam_entity, hit.toi, Some(hit.point), Some(hit.normal));
(entity, hit_data)
})
{
output_events.send(PointerHits::new(
*pointer_id,
vec![(entity, hit_data)],
camera.order as f32,
));
}
let predicate = |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).copied().unwrap_or_default();
let render_layers_match = cam_layers.intersects(&entity_layers);

let pickable = pickables
.get(entity)
.map(|p| *p != Pickable::IGNORE)
.unwrap_or(true);
marker_requirement && render_layers_match && pickable
};
if let Some((entity, hit_data)) = rapier_context
.cast_ray_and_get_normal(
ray.origin,
ray.direction,
f32::MAX,
true,
QueryFilter::new().predicate(&predicate),
)
.map(|(entity, hit)| {
let hit_data =
HitData::new(ray_id.camera, hit.toi, Some(hit.point), Some(hit.normal));
(entity, hit_data)
})
{
output_events.send(PointerHits::new(
ray_id.pointer,
vec![(entity, hit_data)],
camera.order as f32,
));
}
}
}
112 changes: 44 additions & 68 deletions backends/bevy_picking_raycast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ use bevy_app::prelude::*;
use bevy_ecs::prelude::*;
use bevy_reflect::prelude::*;
use bevy_render::{prelude::*, view::RenderLayers};
use bevy_transform::prelude::*;
use bevy_window::{PrimaryWindow, Window};

use bevy_mod_raycast::prelude::*;
use bevy_picking_core::backend::prelude::*;
Expand Down Expand Up @@ -75,81 +73,59 @@ impl Plugin for RaycastBackend {
/// Raycasts into the scene using [`RaycastBackendSettings`] and [`PointerLocation`]s, then outputs
/// [`PointerHits`].
pub fn update_hits(
pointers: Query<(&PointerId, &PointerLocation)>,
primary_window_entity: Query<Entity, With<PrimaryWindow>>,
primary_window: Query<&Window, With<PrimaryWindow>>,
picking_cameras: Query<(
Entity,
&Camera,
&GlobalTransform,
Option<&RaycastPickable>,
Option<&RenderLayers>,
)>,
backend_settings: Res<RaycastBackendSettings>,
ray_map: Res<RayMap>,
picking_cameras: Query<(&Camera, Option<&RaycastPickable>, Option<&RenderLayers>)>,
pickables: Query<&Pickable>,
marked_targets: Query<&RaycastPickable>,
layers: Query<&RenderLayers>,
backend_settings: Res<RaycastBackendSettings>,
mut raycast: Raycast,
mut output_events: EventWriter<PointerHits>,
) {
for (pointer_id, pointer_location) in &pointers {
let pointer_location = match pointer_location.location() {
Some(l) => l,
None => continue,
for (&ray_id, &ray) in ray_map.map().iter() {
let Ok((camera, cam_pickable, cam_layers)) = picking_cameras.get(ray_id.camera) else {
continue;
};
for (cam_entity, camera, ray, cam_layers) in picking_cameras
.iter()
.filter(|(_, camera, ..)| {
camera.is_active && pointer_location.is_in_viewport(camera, &primary_window_entity)
})
.filter(|(.., marker, _)| marker.is_some() || !backend_settings.require_markers)
.filter_map(|(entity, camera, transform, _, layers)| {
Ray3d::from_screenspace(
pointer_location.position,
camera,
transform,
primary_window.single(),
)
.map(|ray| (entity, camera, ray, layers))
})
{
let settings = RaycastSettings {
visibility: backend_settings.raycast_visibility,
filter: &|entity| {
let marker_requirement =
!backend_settings.require_markers || marked_targets.get(entity).is_ok();
if backend_settings.require_markers && cam_pickable.is_none() {
continue;
}

// Cameras missing render layers intersect all layers
let cam_layers = cam_layers.copied().unwrap_or(RenderLayers::all());
// Other entities missing render layers are on the default layer 0
let entity_layers = layers.get(entity).copied().unwrap_or_default();
let render_layers_match = cam_layers.intersects(&entity_layers);
let cam_layers = cam_layers.copied().unwrap_or_default();

marker_requirement && render_layers_match
},
early_exit_test: &|entity_hit| {
pickables
.get(entity_hit)
.is_ok_and(|pickable| pickable.should_block_lower)
},
};
let picks = raycast
.cast_ray(ray, &settings)
.iter()
.map(|(entity, hit)| {
let hit_data = HitData::new(
cam_entity,
hit.distance(),
Some(hit.position()),
Some(hit.normal()),
);
(*entity, hit_data)
})
.collect::<Vec<_>>();
let order = camera.order as f32;
if !picks.is_empty() {
output_events.send(PointerHits::new(*pointer_id, picks, order));
}
let settings = RaycastSettings {
visibility: backend_settings.raycast_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).copied().unwrap_or_default();
let render_layers_match = cam_layers.intersects(&entity_layers);

marker_requirement && render_layers_match
},
early_exit_test: &|entity_hit| {
pickables
.get(entity_hit)
.is_ok_and(|pickable| pickable.should_block_lower)
},
};
let picks = raycast
.cast_ray(ray.into(), &settings)
.iter()
.map(|(entity, hit)| {
let hit_data = HitData::new(
ray_id.camera,
hit.distance(),
Some(hit.position()),
Some(hit.normal()),
);
(*entity, hit_data)
})
.collect::<Vec<_>>();
let order = camera.order as f32;
if !picks.is_empty() {
output_events.send(PointerHits::new(ray_id.pointer, picks, order));
}
}
}
1 change: 1 addition & 0 deletions crates/bevy_picking_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bevy_ecs = { version = "0.12", default-features = false }
bevy_math = { version = "0.12", default-features = false }
bevy_reflect = { version = "0.12", default-features = false }
bevy_render = { version = "0.12", default-features = false }
bevy_transform = { version = "0.12", default-features = false }
bevy_utils = { version = "0.12", default-features = false }
bevy_window = { version = "0.12", default-features = false }

Expand Down
Loading

0 comments on commit f8e0484

Please sign in to comment.