Skip to content

Commit

Permalink
Add physics picking backend using bevy_picking (#554)
Browse files Browse the repository at this point in the history
# Objective

Bevy 0.15 upstreams `bevy_mod_picking` as `bevy_picking`. Now that picking is first-party, we should add a collider picking backend, especially as `bevy_mod_picking` (which has an Avian backend) is on track for deprecation.

## Solution

Add a `PhysicsPickingPlugin`! It is behind the `bevy_picking` default feature and must be added manually, but once it is enabled, picking is enabled for all colliders by default. Picking can be made opt-in using `PhysicsPickingSettings::require_markers`, in which case you must mark pickable entities with the `PhysicsPickable` component. The API and approach quite closely match the `MeshPickingPlugin` in Bevy 0.15.

The plugin is largely based on the [Avian backend](https://github.com/aevyrie/bevy_mod_picking/blob/7494b9614a50a9c25e77a9bdccdb9c010a506ae9/examples/avian.rs) in `bevy_mod_picking`, with some tweaks to docs and naming. I also added a 2D version of the picking backend.

There is a new `picking_3d` example:

https://github.com/user-attachments/assets/5fcac14e-c9ad-40b0-81a6-450fb32ec23b

(Note: The torus and conical frustum use trimesh colliders here, which is why the normals change a bit abruptly)

I have not added a 2D example (yet), but tested that it works:

https://github.com/user-attachments/assets/ec52c846-d4bc-4df5-bfb3-5b155d33492e
  • Loading branch information
Jondolf authored Nov 11, 2024
1 parent 734118b commit 5deada5
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run cargo test
run: cargo test --no-default-features --features enhanced-determinism,collider-from-mesh,serialize,debug-plugin,avian2d/2d,avian3d/3d,avian2d/f64,avian3d/f64,default-collider,parry-f64,bevy_scene
run: cargo test --no-default-features --features enhanced-determinism,collider-from-mesh,serialize,debug-plugin,avian2d/2d,avian3d/3d,avian2d/f64,avian3d/f64,default-collider,parry-f64,bevy_scene,bevy_picking

lints:
name: Lints
Expand Down
11 changes: 10 additions & 1 deletion crates/avian2d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ keywords = ["gamedev", "physics", "simulation", "bevy"]
categories = ["game-development", "science", "simulation"]

[features]
default = ["2d", "f32", "parry-f32", "debug-plugin", "parallel", "bevy_scene"]
default = [
"2d",
"f32",
"parry-f32",
"debug-plugin",
"parallel",
"bevy_scene",
"bevy_picking",
]
2d = []
f32 = []
f64 = []
Expand All @@ -34,6 +42,7 @@ parry-f32 = ["f32", "dep:parry2d", "default-collider"]
parry-f64 = ["f64", "dep:parry2d-f64", "default-collider"]

bevy_scene = ["bevy/bevy_scene"]
bevy_picking = ["bevy/bevy_picking"]
serialize = [
"dep:serde",
"bevy/serialize",
Expand Down
6 changes: 6 additions & 0 deletions crates/avian3d/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ default = [
"parry-f32",
"collider-from-mesh",
"bevy_scene",
"bevy_picking",
"debug-plugin",
"parallel",
]
Expand All @@ -43,6 +44,7 @@ parry-f64 = ["f64", "dep:parry3d-f64", "default-collider"]

collider-from-mesh = ["bevy/bevy_render", "3d"]
bevy_scene = ["bevy/bevy_scene"]
bevy_picking = ["bevy/bevy_picking"]
serialize = [
"dep:serde",
"bevy/serialize",
Expand Down Expand Up @@ -128,6 +130,10 @@ required-features = ["3d", "default-collider"]
name = "revolute_joint_3d"
required-features = ["3d", "default-collider"]

[[example]]
name = "picking"
required-features = ["3d", "default-collider", "bevy_picking"]

[[example]]
name = "trimesh_shapes_3d"
required-features = ["3d", "default-collider", "bevy_scene"]
Expand Down
172 changes: 172 additions & 0 deletions crates/avian3d/examples/picking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//! A simple 3D scene to demonstrate physics picking for colliders.
//!
//! By default, the [`PhysicsPickingPlugin`] will test intersections with the pointer against all colliders.
//! If you want physics picking to be opt-in, you can set [`PhysicsPickingSettings::require_markers`] to `true`
//! and add a [`PhysicsPickable`] component to the desired camera and target entities.

use std::f32::consts::PI;

use avian3d::{math::Vector, prelude::*};
use bevy::{color::palettes::tailwind::*, picking::pointer::PointerInteraction, prelude::*};

fn main() {
App::new()
.add_plugins((
DefaultPlugins,
PhysicsPlugins::default(),
// `PhysicsPickingPlugin` is not a default plugin
PhysicsPickingPlugin,
))
.add_systems(Startup, setup_scene)
.add_systems(Update, draw_pointer_intersections)
.run();
}

/// 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 = 12.0;
const Z_EXTENT: f32 = 5.0;

fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Set up the materials.
let white_matl = materials.add(Color::WHITE);
let ground_matl = materials.add(Color::from(GRAY_300));
let hover_matl = materials.add(Color::from(CYAN_300));
let pressed_matl = materials.add(Color::from(YELLOW_300));

// Meshes and colliders for the shapes.
let shapes = [
(
meshes.add(Cuboid::default()),
Collider::from(Cuboid::default()),
),
(
meshes.add(Tetrahedron::default()),
Collider::convex_hull_from_mesh(&Tetrahedron::default().mesh().build()).unwrap(),
),
(
meshes.add(Capsule3d::default()),
Collider::from(Capsule3d::default()),
),
(
meshes.add(Torus::default()),
Collider::trimesh_from_mesh(&Torus::default().mesh().build()).unwrap(),
),
(
meshes.add(Cylinder::default()),
Collider::from(Cylinder::default()),
),
(meshes.add(Cone::default()), Collider::from(Cone::default())),
(
meshes.add(ConicalFrustum::default()),
Collider::trimesh_from_mesh(&ConicalFrustum::default().mesh().build()).unwrap(),
),
(
meshes.add(Sphere::default().mesh().ico(5).unwrap()),
Collider::from(Sphere::default()),
),
];

let num_shapes = shapes.len();

// Spawn the shapes. The colliders will be pickable by default.
for (i, (mesh, collider)) in shapes.into_iter().enumerate() {
commands
.spawn((
Mesh3d(mesh),
MeshMaterial3d(white_matl.clone()),
RigidBody::Kinematic,
collider,
AngularVelocity(Vector::new(0.0, 0.5, 0.0)),
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(update_material_on::<Pointer<Over>>(hover_matl.clone()))
.observe(update_material_on::<Pointer<Out>>(white_matl.clone()))
.observe(update_material_on::<Pointer<Down>>(pressed_matl.clone()))
.observe(update_material_on::<Pointer<Up>>(hover_matl.clone()))
.observe(rotate_on_drag);
}

// Ground
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))),
MeshMaterial3d(ground_matl.clone()),
RigidBody::Static,
Collider::cuboid(50.0, 0.1, 50.0),
PickingBehavior::IGNORE, // Disable picking for the ground plane.
));

// 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\nDrag to rotate"),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}

/// Returns an observer that updates the entity's material to the one specified.
fn update_material_on<E>(
new_material: Handle<StandardMaterial>,
) -> impl Fn(Trigger<E>, Query<&mut MeshMaterial3d<StandardMaterial>>) {
// An observer closure that captures `new_material`. We do this to avoid needing to write four
// versions of this observer, each triggered by a different event and with a different hardcoded
// material. Instead, the event type is a generic, and the material is passed in.
move |trigger, mut query| {
if let Ok(mut material) = query.get_mut(trigger.entity()) {
material.0 = new_material.clone();
}
}
}

/// A system that draws hit indicators for every pointer.
fn draw_pointer_intersections(pointers: Query<&PointerInteraction>, mut gizmos: Gizmos) {
for (point, normal) in pointers
.iter()
.filter_map(|interaction| interaction.get_nearest_hit())
.filter_map(|(_entity, hit)| hit.position.zip(hit.normal))
{
gizmos.sphere(point, 0.05, RED_500);
gizmos.arrow(point, point + normal.normalize() * 0.5, PINK_100);
}
}

/// An observer to rotate an entity when it is dragged.
fn rotate_on_drag(drag: Trigger<Pointer<Drag>>, mut transforms: Query<&mut Transform>) {
let mut transform = transforms.get_mut(drag.entity()).unwrap();
transform.rotate_y(drag.delta.x * 0.02);
transform.rotate_x(drag.delta.y * 0.02);
}
37 changes: 21 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,26 @@
//!
//! ### Feature flags
//!
//! | Feature | Description | Default feature |
//! | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
//! | `2d` | Enables 2D physics. Incompatible with `3d`. | Yes (`avian2d`) |
//! | `3d` | Enables 3D physics. Incompatible with `2d`. | Yes (`avian3d`) |
//! | `f32` | Enables `f32` precision for physics. Incompatible with `f64`. | Yes |
//! | `f64` | Enables `f64` precision for physics. Incompatible with `f32`. | No |
//! | `default-collider` | Enables the default [`Collider`]. Required for [spatial queries](spatial_query). Requires either the `parry-f32` or `parry-f64` feature. | Yes |
//! | `parry-f32` | Enables the `f32` version of the Parry collision detection library. Also enables the `default-collider` feature. | Yes |
//! | `parry-f64` | Enables the `f64` version of the Parry collision detection library. Also enables the `default-collider` feature. | No |
//! | Feature | Description | Default feature |
//! | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
//! | `2d` | Enables 2D physics. Incompatible with `3d`. | Yes (`avian2d`) |
//! | `3d` | Enables 3D physics. Incompatible with `2d`. | Yes (`avian3d`) |
//! | `f32` | Enables `f32` precision for physics. Incompatible with `f64`. | Yes |
//! | `f64` | Enables `f64` precision for physics. Incompatible with `f32`. | No |
//! | `default-collider` | Enables the default [`Collider`]. Required for [spatial queries](spatial_query). Requires either the `parry-f32` or `parry-f64` feature. | Yes |
//! | `parry-f32` | Enables the `f32` version of the Parry collision detection library. Also enables the `default-collider` feature. | Yes |
//! | `parry-f64` | Enables the `f64` version of the Parry collision detection library. Also enables the `default-collider` feature. | No |
#![cfg_attr(
feature = "3d",
doc = "| `collider-from-mesh` | Allows you to create [`Collider`]s from `Mesh`es. | Yes |"
doc = "| `collider-from-mesh` | Allows you to create [`Collider`]s from `Mesh`es. | Yes |"
)]
//! | `bevy_scene` | Enables [`ColliderConstructorHierarchy`] to wait until a [`Scene`] has loaded before processing it. | Yes |
//! | `debug-plugin` | Enables physics debug rendering using the [`PhysicsDebugPlugin`]. The plugin must be added separately. | Yes |
//! | `enhanced-determinism` | Enables increased determinism. | No |
//! | `parallel` | Enables some extra multithreading, which improves performance for larger simulations but can add some overhead for smaller ones. | Yes |
//! | `simd` | Enables [SIMD] optimizations. | No |
//! | `serialize` | Enables support for serialization and deserialization using Serde. | No |
//! | `bevy_scene` | Enables [`ColliderConstructorHierarchy`] to wait until a [`Scene`] has loaded before processing it. | Yes |
//! | `bevy_picking` | Enables physics picking support for `bevy_picking` using the [`PhysicsPickingPlugin`]. The plugin must be added separately. | Yes |
//! | `debug-plugin` | Enables physics debug rendering using the [`PhysicsDebugPlugin`]. The plugin must be added separately. | Yes |
//! | `enhanced-determinism` | Enables increased determinism. | No |
//! | `parallel` | Enables some extra multithreading, which improves performance for larger simulations but can add some overhead for smaller ones. | Yes |
//! | `simd` | Enables [SIMD] optimizations. | No |
//! | `serialize` | Enables support for serialization and deserialization using Serde. | No |
//!
//! [SIMD]: https://en.wikipedia.org/wiki/Single_instruction,_multiple_data
//!
Expand Down Expand Up @@ -451,6 +452,8 @@ pub mod collision;
pub mod debug_render;
pub mod dynamics;
pub mod math;
#[cfg(feature = "bevy_picking")]
pub mod picking;
pub mod position;
pub mod prepare;
pub mod schedule;
Expand All @@ -464,6 +467,8 @@ pub use type_registration::PhysicsTypeRegistrationPlugin;
pub mod prelude {
#[cfg(feature = "debug-plugin")]
pub use crate::debug_render::*;
#[cfg(feature = "bevy_picking")]
pub use crate::picking::{PhysicsPickable, PhysicsPickingPlugin, PhysicsPickingSettings};
#[cfg(feature = "default-collider")]
pub(crate) use crate::position::RotationValue;
pub use crate::{
Expand Down
Loading

0 comments on commit 5deada5

Please sign in to comment.