mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Upstream CorePlugin
from bevy_mod_picking
(#13677)
# Objective This is the first of a series of PRs intended to begin the upstreaming process for `bevy_mod_picking`. The purpose of this PR is to: + Create the new `bevy_picking` crate + Upstream `CorePlugin` as `PickingPlugin` + Upstream the core pointer and backend abstractions. This code has been ported verbatim from the corresponding files in [bevy_picking_core](https://github.com/aevyrie/bevy_mod_picking/tree/main/crates/bevy_picking_core/src) with a few tiny naming and docs tweaks. The work here is only an initial foothold to get the up-streaming process started in earnest. We can do refactoring and improvements once this is in-tree. --------- Co-authored-by: Aevyrie <aevyrie@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
eb3c81374a
commit
aaccbe88aa
10 changed files with 799 additions and 4 deletions
|
@ -63,6 +63,7 @@ default = [
|
||||||
"bevy_winit",
|
"bevy_winit",
|
||||||
"bevy_core_pipeline",
|
"bevy_core_pipeline",
|
||||||
"bevy_pbr",
|
"bevy_pbr",
|
||||||
|
"bevy_picking",
|
||||||
"bevy_gltf",
|
"bevy_gltf",
|
||||||
"bevy_render",
|
"bevy_render",
|
||||||
"bevy_sprite",
|
"bevy_sprite",
|
||||||
|
@ -123,6 +124,9 @@ bevy_pbr = [
|
||||||
"bevy_core_pipeline",
|
"bevy_core_pipeline",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Provides picking functionality
|
||||||
|
bevy_picking = ["bevy_internal/bevy_picking"]
|
||||||
|
|
||||||
# Provides rendering functionality
|
# Provides rendering functionality
|
||||||
bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
|
bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,9 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
|
||||||
# Provides a collection of developer tools
|
# Provides a collection of developer tools
|
||||||
bevy_dev_tools = ["dep:bevy_dev_tools"]
|
bevy_dev_tools = ["dep:bevy_dev_tools"]
|
||||||
|
|
||||||
|
# Provides a picking functionality
|
||||||
|
bevy_picking = ["dep:bevy_picking"]
|
||||||
|
|
||||||
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||||
|
|
||||||
|
@ -214,18 +217,19 @@ bevy_asset = { path = "../bevy_asset", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.14.0-dev" }
|
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_color = { path = "../bevy_color", optional = true, version = "0.14.0-dev" }
|
bevy_color = { path = "../bevy_color", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.14.0-dev" }
|
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.14.0-dev" }
|
||||||
|
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
|
||||||
|
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
|
||||||
|
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
|
||||||
|
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
|
||||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0-dev" }
|
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.14.0-dev" }
|
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.14.0-dev" }
|
||||||
|
bevy_picking = { path = "../bevy_picking", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_render = { path = "../bevy_render", optional = true, version = "0.14.0-dev" }
|
bevy_render = { path = "../bevy_render", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
|
|
||||||
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.14.0-dev" }
|
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.14.0-dev" }
|
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_text = { path = "../bevy_text", optional = true, version = "0.14.0-dev" }
|
bevy_text = { path = "../bevy_text", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0-dev" }
|
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0-dev" }
|
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0-dev" }
|
||||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
|
|
||||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
|
|
||||||
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -44,6 +44,8 @@ pub use bevy_log as log;
|
||||||
pub use bevy_math as math;
|
pub use bevy_math as math;
|
||||||
#[cfg(feature = "bevy_pbr")]
|
#[cfg(feature = "bevy_pbr")]
|
||||||
pub use bevy_pbr as pbr;
|
pub use bevy_pbr as pbr;
|
||||||
|
#[cfg(feature = "bevy_picking")]
|
||||||
|
pub use bevy_picking as picking;
|
||||||
pub use bevy_ptr as ptr;
|
pub use bevy_ptr as ptr;
|
||||||
pub use bevy_reflect as reflect;
|
pub use bevy_reflect as reflect;
|
||||||
#[cfg(feature = "bevy_render")]
|
#[cfg(feature = "bevy_render")]
|
||||||
|
|
27
crates/bevy_picking/Cargo.toml
Normal file
27
crates/bevy_picking/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "bevy_picking"
|
||||||
|
version = "0.14.0-dev"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Provides screen picking functionality for Bevy Engine"
|
||||||
|
homepage = "https://bevyengine.org"
|
||||||
|
repository = "https://github.com/bevyengine/bevy"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||||
|
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||||
|
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||||
|
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
|
||||||
|
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||||
|
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||||
|
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||||
|
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
||||||
|
|
||||||
|
uuid = { version = "1.1", features = ["v4"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"]
|
||||||
|
all-features = true
|
1
crates/bevy_picking/README.md
Normal file
1
crates/bevy_picking/README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Bevy Picking
|
232
crates/bevy_picking/src/backend.rs
Normal file
232
crates/bevy_picking/src/backend.rs
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
//! This module provides a simple interface for implementing a picking backend.
|
||||||
|
//!
|
||||||
|
//! Don't be dissuaded by terminology like "backend"; the idea is dead simple. `bevy_picking`
|
||||||
|
//! will tell you where pointers are, all you have to do is send an event if the pointers are
|
||||||
|
//! hitting something. That's it. The rest of this documentation explains the requirements in more
|
||||||
|
//! detail.
|
||||||
|
//!
|
||||||
|
//! Because `bevy_picking` is very loosely coupled with its backends, you can mix and match as
|
||||||
|
//! many backends as you want. For example, You could use the `rapier` backend to raycast against
|
||||||
|
//! physics objects, a picking shader backend to pick non-physics meshes, and the `bevy_ui` backend
|
||||||
|
//! for your UI. The [`PointerHits`]s produced by these various backends will be combined, sorted,
|
||||||
|
//! and used as a homogeneous input for the picking systems that consume these events.
|
||||||
|
//!
|
||||||
|
//! ## Implementation
|
||||||
|
//!
|
||||||
|
//! - A picking backend only has one job: read [`PointerLocation`](crate::pointer::PointerLocation)
|
||||||
|
//! components and produce [`PointerHits`] events. In plain English, a backend is provided the
|
||||||
|
//! location of pointers, and is asked to provide a list of entities under those pointers.
|
||||||
|
//!
|
||||||
|
//! - The [`PointerHits`] events produced by a backend do **not** need to be sorted or filtered, all
|
||||||
|
//! that is needed is an unordered list of entities and their [`HitData`].
|
||||||
|
//!
|
||||||
|
//! - Backends do not need to consider the [`Pickable`](crate::Pickable) component, though they may
|
||||||
|
//! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy
|
||||||
|
//! may want to early exit if it intersects an entity that blocks lower entities from being
|
||||||
|
//! picked.
|
||||||
|
//!
|
||||||
|
//! ### Raycasting Backends
|
||||||
|
//!
|
||||||
|
//! Backends that require a ray to cast into the scene should use [`ray::RayMap`]. This
|
||||||
|
//! automatically constructs rays in world space for all cameras and pointers, handling details like
|
||||||
|
//! viewports and DPI for you.
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_math::Vec3;
|
||||||
|
use bevy_reflect::Reflect;
|
||||||
|
|
||||||
|
/// Common imports for implementing a picking backend.
|
||||||
|
pub mod prelude {
|
||||||
|
pub use super::{ray::RayMap, HitData, PointerHits};
|
||||||
|
pub use crate::{
|
||||||
|
pointer::{PointerId, PointerLocation},
|
||||||
|
PickSet, Pickable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event produced by a picking backend after it has run its hit tests, describing the entities
|
||||||
|
/// under a pointer.
|
||||||
|
///
|
||||||
|
/// Some backends may only support providing the topmost entity; this is a valid limitation of some
|
||||||
|
/// backends. For example, a picking shader might only have data on the topmost rendered output from
|
||||||
|
/// its buffer.
|
||||||
|
#[derive(Event, Debug, Clone)]
|
||||||
|
pub struct PointerHits {
|
||||||
|
/// The pointer associated with this hit test.
|
||||||
|
pub pointer: prelude::PointerId,
|
||||||
|
/// An unordered collection of entities and their distance (depth) from the cursor.
|
||||||
|
pub picks: Vec<(Entity, HitData)>,
|
||||||
|
/// Set the order of this group of picks. Normally, this is the
|
||||||
|
/// [`bevy_render::camera::Camera::order`].
|
||||||
|
///
|
||||||
|
/// Used to allow multiple `PointerHits` submitted for the same pointer to be ordered.
|
||||||
|
/// `PointerHits` with a higher `order` will be checked before those with a lower `order`,
|
||||||
|
/// regardless of the depth of each entity pick.
|
||||||
|
///
|
||||||
|
/// In other words, when pick data is coalesced across all backends, the data is grouped by
|
||||||
|
/// pointer, then sorted by order, and checked sequentially, sorting each `PointerHits` by
|
||||||
|
/// entity depth. Events with a higher `order` are effectively on top of events with a lower
|
||||||
|
/// order.
|
||||||
|
///
|
||||||
|
/// ### Why is this an `f32`???
|
||||||
|
///
|
||||||
|
/// Bevy UI is special in that it can share a camera with other things being rendered. in order
|
||||||
|
/// to properly sort them, we need a way to make `bevy_ui`'s order a tiny bit higher, like adding
|
||||||
|
/// 0.5 to the order. We can't use integers, and we want users to be using camera.order by
|
||||||
|
/// default, so this is the best solution at the moment.
|
||||||
|
pub order: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerHits {
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
pointer,
|
||||||
|
picks,
|
||||||
|
order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds data from a successful pointer hit test. See [`HitData::depth`] for important details.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Reflect)]
|
||||||
|
pub struct HitData {
|
||||||
|
/// The camera entity used to detect this hit. Useful when you need to find the ray that was
|
||||||
|
/// casted for this hit when using a raycasting backend.
|
||||||
|
pub camera: Entity,
|
||||||
|
/// `depth` only needs to be self-consistent with other [`PointerHits`]s using the same
|
||||||
|
/// [`RenderTarget`](bevy_render::camera::RenderTarget). However, it is recommended to use the
|
||||||
|
/// distance from the pointer to the hit, measured from the near plane of the camera, to the
|
||||||
|
/// point, in world space.
|
||||||
|
pub depth: f32,
|
||||||
|
/// The position of the intersection in the world, if the data is available from the backend.
|
||||||
|
pub position: Option<Vec3>,
|
||||||
|
/// The normal vector of the hit test, if the data is available from the backend.
|
||||||
|
pub normal: Option<Vec3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HitData {
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub fn new(camera: Entity, depth: f32, position: Option<Vec3>, normal: Option<Vec3>) -> Self {
|
||||||
|
Self {
|
||||||
|
camera,
|
||||||
|
depth,
|
||||||
|
position,
|
||||||
|
normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod ray {
|
||||||
|
//! Types and systems for constructing rays from cameras and pointers.
|
||||||
|
|
||||||
|
use crate::backend::prelude::{PointerId, PointerLocation};
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_math::Ray3d;
|
||||||
|
use bevy_reflect::Reflect;
|
||||||
|
use bevy_render::camera::Camera;
|
||||||
|
use bevy_transform::prelude::GlobalTransform;
|
||||||
|
use bevy_utils::{hashbrown::hash_map::Iter, HashMap};
|
||||||
|
use bevy_window::PrimaryWindow;
|
||||||
|
|
||||||
|
/// Identifies a ray constructed from some (pointer, camera) combination. A pointer can be over
|
||||||
|
/// multiple cameras, which is why a single pointer may have multiple rays.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Reflect)]
|
||||||
|
pub struct RayId {
|
||||||
|
/// The camera whose projection was used to calculate the ray.
|
||||||
|
pub camera: Entity,
|
||||||
|
/// The pointer whose pixel coordinates were used to calculate the ray.
|
||||||
|
pub pointer: PointerId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RayId {
|
||||||
|
/// Construct a [`RayId`].
|
||||||
|
pub fn new(camera: Entity, pointer: PointerId) -> Self {
|
||||||
|
Self { camera, pointer }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A map from [`RayId`] to [`Ray3d`].
|
||||||
|
///
|
||||||
|
/// This map is cleared and re-populated every frame before any backends run. Ray-based picking
|
||||||
|
/// backends should use this when possible, as it automatically handles viewports, DPI, and
|
||||||
|
/// other details of building rays from pointer locations.
|
||||||
|
///
|
||||||
|
/// ## Usage
|
||||||
|
///
|
||||||
|
/// Iterate over each [`Ray3d`] and its [`RayId`] with [`RayMap::iter`].
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use bevy_ecs::prelude::*;
|
||||||
|
/// # use bevy_picking::backend::ray::RayMap;
|
||||||
|
/// # use bevy_picking::backend::PointerHits;
|
||||||
|
/// // My raycasting backend
|
||||||
|
/// pub fn update_hits(ray_map: Res<RayMap>, mut output_events: EventWriter<PointerHits>,) {
|
||||||
|
/// for (&ray_id, &ray) in ray_map.iter() {
|
||||||
|
/// // Run a raycast with each ray, returning any `PointerHits` found.
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug, Default, Resource)]
|
||||||
|
pub struct RayMap {
|
||||||
|
map: HashMap<RayId, Ray3d>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RayMap {
|
||||||
|
/// Iterates over all world space rays for every picking pointer.
|
||||||
|
pub fn iter(&self) -> Iter<'_, RayId, Ray3d> {
|
||||||
|
self.map.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hash map of all rays cast in the current frame.
|
||||||
|
pub fn map(&self) -> &HashMap<RayId, Ray3d> {
|
||||||
|
&self.map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the [`RayMap`] and re-populates it with one ray for each
|
||||||
|
/// combination of pointer entity and camera entity where the pointer
|
||||||
|
/// intersects the camera's viewport.
|
||||||
|
pub fn repopulate(
|
||||||
|
mut ray_map: ResMut<Self>,
|
||||||
|
primary_window_entity: Query<Entity, With<PrimaryWindow>>,
|
||||||
|
cameras: Query<(Entity, &Camera, &GlobalTransform)>,
|
||||||
|
pointers: Query<(&PointerId, &PointerLocation)>,
|
||||||
|
) {
|
||||||
|
ray_map.map.clear();
|
||||||
|
|
||||||
|
for (camera_entity, camera, camera_tfm) in &cameras {
|
||||||
|
if !camera.is_active {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (&pointer_id, pointer_loc) in &pointers {
|
||||||
|
if let Some(ray) =
|
||||||
|
make_ray(&primary_window_entity, camera, camera_tfm, pointer_loc)
|
||||||
|
{
|
||||||
|
ray_map
|
||||||
|
.map
|
||||||
|
.insert(RayId::new(camera_entity, pointer_id), ray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_ray(
|
||||||
|
primary_window_entity: &Query<Entity, With<PrimaryWindow>>,
|
||||||
|
camera: &Camera,
|
||||||
|
camera_tfm: &GlobalTransform,
|
||||||
|
pointer_loc: &PointerLocation,
|
||||||
|
) -> Option<Ray3d> {
|
||||||
|
let pointer_loc = pointer_loc.location()?;
|
||||||
|
if !pointer_loc.is_in_viewport(camera, primary_window_entity) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut viewport_pos = pointer_loc.position;
|
||||||
|
if let Some(viewport) = &camera.viewport {
|
||||||
|
let viewport_logical = camera.to_logical(viewport.physical_position)?;
|
||||||
|
viewport_pos -= viewport_logical;
|
||||||
|
}
|
||||||
|
camera.viewport_to_world(camera_tfm, viewport_pos)
|
||||||
|
}
|
||||||
|
}
|
212
crates/bevy_picking/src/lib.rs
Normal file
212
crates/bevy_picking/src/lib.rs
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
//! TODO, write module doc
|
||||||
|
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
pub mod backend;
|
||||||
|
pub mod pointer;
|
||||||
|
|
||||||
|
use bevy_app::prelude::*;
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
|
||||||
|
/// Used to globally toggle picking features at runtime.
|
||||||
|
#[derive(Clone, Debug, Resource, Reflect)]
|
||||||
|
#[reflect(Resource, Default)]
|
||||||
|
pub struct PickingPluginsSettings {
|
||||||
|
/// Enables and disables all picking features.
|
||||||
|
pub is_enabled: bool,
|
||||||
|
/// Enables and disables input collection.
|
||||||
|
pub is_input_enabled: bool,
|
||||||
|
/// Enables and disables updating interaction states of entities.
|
||||||
|
pub is_focus_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickingPluginsSettings {
|
||||||
|
/// Whether or not input collection systems should be running.
|
||||||
|
pub fn input_should_run(state: Res<Self>) -> bool {
|
||||||
|
state.is_input_enabled && state.is_enabled
|
||||||
|
}
|
||||||
|
/// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction)
|
||||||
|
/// component should be running.
|
||||||
|
pub fn focus_should_run(state: Res<Self>) -> bool {
|
||||||
|
state.is_focus_enabled && state.is_enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PickingPluginsSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
is_enabled: true,
|
||||||
|
is_input_enabled: true,
|
||||||
|
is_focus_enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An optional component that overrides default picking behavior for an entity, allowing you to
|
||||||
|
/// make an entity non-hoverable, or allow items below it to be hovered. See the documentation on
|
||||||
|
/// the fields for more details.
|
||||||
|
#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub struct Pickable {
|
||||||
|
/// Should this entity block entities below it from being picked?
|
||||||
|
///
|
||||||
|
/// This is useful if you want picking to continue hitting entities below this one. Normally,
|
||||||
|
/// only the topmost entity under a pointer can be hovered, but this setting allows the pointer
|
||||||
|
/// to hover multiple entities, from nearest to farthest, stopping as soon as it hits an entity
|
||||||
|
/// that blocks lower entities.
|
||||||
|
///
|
||||||
|
/// Note that the word "lower" here refers to entities that have been reported as hit by any
|
||||||
|
/// picking backend, but are at a lower depth than the current one. This is different from the
|
||||||
|
/// concept of event bubbling, as it works irrespective of the entity hierarchy.
|
||||||
|
///
|
||||||
|
/// For example, if a pointer is over a UI element, as well as a 3d mesh, backends will report
|
||||||
|
/// hits for both of these entities. Additionally, the hits will be sorted by the camera order,
|
||||||
|
/// so if the UI is drawing on top of the 3d mesh, the UI will be "above" the mesh. When focus
|
||||||
|
/// is computed, the UI element will be checked first to see if it this field is set to block
|
||||||
|
/// lower entities. If it does (default), the focus system will stop there, and only the UI
|
||||||
|
/// element will be marked as hovered. However, if this field is set to `false`, both the UI
|
||||||
|
/// element *and* the mesh will be marked as hovered.
|
||||||
|
///
|
||||||
|
/// Entities without the [`Pickable`] component will block by default.
|
||||||
|
pub should_block_lower: bool,
|
||||||
|
/// Should this entity be added to the [`HoverMap`](focus::HoverMap) and thus emit events when
|
||||||
|
/// targeted?
|
||||||
|
///
|
||||||
|
/// If this is set to `false` and `should_block_lower` is set to true, this entity will block
|
||||||
|
/// lower entities from being interacted and at the same time will itself not emit any events.
|
||||||
|
///
|
||||||
|
/// Note that the word "lower" here refers to entities that have been reported as hit by any
|
||||||
|
/// picking backend, but are at a lower depth than the current one. This is different from the
|
||||||
|
/// concept of event bubbling, as it works irrespective of the entity hierarchy.
|
||||||
|
///
|
||||||
|
/// For example, if a pointer is over a UI element, and this field is set to `false`, it will
|
||||||
|
/// not be marked as hovered, and consequently will not emit events nor will any picking
|
||||||
|
/// components mark it as hovered. This can be combined with the other field
|
||||||
|
/// [`Self::should_block_lower`], which is orthogonal to this one.
|
||||||
|
///
|
||||||
|
/// Entities without the [`Pickable`] component are hoverable by default.
|
||||||
|
pub is_hoverable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pickable {
|
||||||
|
/// This entity will not block entities beneath it, nor will it emit events.
|
||||||
|
///
|
||||||
|
/// If a backend reports this entity as being hit, the picking plugin will completely ignore it.
|
||||||
|
pub const IGNORE: Self = Self {
|
||||||
|
should_block_lower: false,
|
||||||
|
is_hoverable: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Pickable {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
should_block_lower: true,
|
||||||
|
is_hoverable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Components needed to build a pointer. Multiple pointers can be active at once, with each pointer
|
||||||
|
/// being an entity.
|
||||||
|
///
|
||||||
|
/// `Mouse` and `Touch` pointers are automatically spawned as needed. Use this bundle if you are
|
||||||
|
/// spawning a custom `PointerId::Custom` pointer, either for testing, as a software controlled
|
||||||
|
/// pointer, or if you are replacing the default touch and mouse inputs.
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct PointerBundle {
|
||||||
|
/// The pointer's unique [`PointerId`](pointer::PointerId).
|
||||||
|
pub id: pointer::PointerId,
|
||||||
|
/// Tracks the pointer's location.
|
||||||
|
pub location: pointer::PointerLocation,
|
||||||
|
/// Tracks the pointer's button press state.
|
||||||
|
pub click: pointer::PointerPress,
|
||||||
|
/// The interaction state of any hovered entities.
|
||||||
|
pub interaction: pointer::PointerInteraction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerBundle {
|
||||||
|
/// Create a new pointer with the provided [`PointerId`](pointer::PointerId).
|
||||||
|
pub fn new(id: pointer::PointerId) -> Self {
|
||||||
|
PointerBundle {
|
||||||
|
id,
|
||||||
|
location: pointer::PointerLocation::default(),
|
||||||
|
click: pointer::PointerPress::default(),
|
||||||
|
interaction: pointer::PointerInteraction::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the location of the pointer bundle
|
||||||
|
pub fn with_location(mut self, location: pointer::Location) -> Self {
|
||||||
|
self.location.location = Some(location);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Groups the stages of the picking process under shared labels.
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||||
|
pub enum PickSet {
|
||||||
|
/// Produces pointer input events. In the [`First`] schedule.
|
||||||
|
Input,
|
||||||
|
/// Runs after input events are generated but before commands are flushed. In the [`First`]
|
||||||
|
/// schedule.
|
||||||
|
PostInput,
|
||||||
|
/// Receives and processes pointer input events. In the [`PreUpdate`] schedule.
|
||||||
|
ProcessInput,
|
||||||
|
/// Reads inputs and produces [`backend::PointerHits`]s. In the [`PreUpdate`] schedule.
|
||||||
|
Backend,
|
||||||
|
/// Reads [`backend::PointerHits`]s, and updates focus, selection, and highlighting states. In
|
||||||
|
/// the [`PreUpdate`] schedule.
|
||||||
|
Focus,
|
||||||
|
/// Runs after all the focus systems are done, before event listeners are triggered. In the
|
||||||
|
/// [`PreUpdate`] schedule.
|
||||||
|
PostFocus,
|
||||||
|
/// Runs after all other picking sets. In the [`PreUpdate`] schedule.
|
||||||
|
Last,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
|
||||||
|
/// types used by other picking plugins.
|
||||||
|
pub struct PickingPlugin;
|
||||||
|
|
||||||
|
impl Plugin for PickingPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<PickingPluginsSettings>()
|
||||||
|
.init_resource::<pointer::PointerMap>()
|
||||||
|
.init_resource::<backend::ray::RayMap>()
|
||||||
|
.add_event::<pointer::InputPress>()
|
||||||
|
.add_event::<pointer::InputMove>()
|
||||||
|
.add_event::<backend::PointerHits>()
|
||||||
|
.add_systems(
|
||||||
|
PreUpdate,
|
||||||
|
(
|
||||||
|
pointer::update_pointer_map,
|
||||||
|
pointer::InputMove::receive,
|
||||||
|
pointer::InputPress::receive,
|
||||||
|
backend::ray::RayMap::repopulate,
|
||||||
|
)
|
||||||
|
.in_set(PickSet::ProcessInput),
|
||||||
|
)
|
||||||
|
.configure_sets(First, (PickSet::Input, PickSet::PostInput).chain())
|
||||||
|
.configure_sets(
|
||||||
|
PreUpdate,
|
||||||
|
(
|
||||||
|
PickSet::ProcessInput.run_if(PickingPluginsSettings::input_should_run),
|
||||||
|
PickSet::Backend,
|
||||||
|
PickSet::Focus.run_if(PickingPluginsSettings::focus_should_run),
|
||||||
|
PickSet::PostFocus,
|
||||||
|
// Eventually events will need to be dispatched here
|
||||||
|
PickSet::Last,
|
||||||
|
)
|
||||||
|
.chain(),
|
||||||
|
)
|
||||||
|
.register_type::<pointer::PointerId>()
|
||||||
|
.register_type::<pointer::PointerLocation>()
|
||||||
|
.register_type::<pointer::PointerPress>()
|
||||||
|
.register_type::<pointer::PointerInteraction>()
|
||||||
|
.register_type::<Pickable>()
|
||||||
|
.register_type::<PickingPluginsSettings>()
|
||||||
|
.register_type::<backend::ray::RayId>();
|
||||||
|
}
|
||||||
|
}
|
311
crates/bevy_picking/src/pointer.rs
Normal file
311
crates/bevy_picking/src/pointer.rs
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
//! Types and systems for pointer inputs, such as position and buttons.
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_math::{Rect, Vec2};
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_render::camera::{Camera, NormalizedRenderTarget};
|
||||||
|
use bevy_utils::HashMap;
|
||||||
|
use bevy_window::PrimaryWindow;
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use crate::backend::HitData;
|
||||||
|
|
||||||
|
/// Identifies a unique pointer entity. `Mouse` and `Touch` pointers are automatically spawned.
|
||||||
|
///
|
||||||
|
/// This component is needed because pointers can be spawned and despawned, but they need to have a
|
||||||
|
/// stable ID that persists regardless of the Entity they are associated with.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Component, Reflect)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub enum PointerId {
|
||||||
|
/// The mouse pointer.
|
||||||
|
#[default]
|
||||||
|
Mouse,
|
||||||
|
/// A touch input, usually numbered by window touch events from `winit`.
|
||||||
|
Touch(u64),
|
||||||
|
/// A custom, uniquely identified pointer. Useful for mocking inputs or implementing a software
|
||||||
|
/// controlled cursor.
|
||||||
|
#[reflect(ignore)]
|
||||||
|
Custom(Uuid),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerId {
|
||||||
|
/// Returns true if the pointer is a touch input.
|
||||||
|
pub fn is_touch(&self) -> bool {
|
||||||
|
matches!(self, PointerId::Touch(_))
|
||||||
|
}
|
||||||
|
/// Returns true if the pointer is the mouse.
|
||||||
|
pub fn is_mouse(&self) -> bool {
|
||||||
|
matches!(self, PointerId::Mouse)
|
||||||
|
}
|
||||||
|
/// Returns true if the pointer is a custom input.
|
||||||
|
pub fn is_custom(&self) -> bool {
|
||||||
|
matches!(self, PointerId::Custom(_))
|
||||||
|
}
|
||||||
|
/// Returns the touch id if the pointer is a touch input.
|
||||||
|
pub fn get_touch_id(&self) -> Option<u64> {
|
||||||
|
if let PointerId::Touch(id) = self {
|
||||||
|
Some(*id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds a list of entities this pointer is currently interacting with, sorted from nearest to
|
||||||
|
/// farthest.
|
||||||
|
#[derive(Debug, Default, Clone, Component, Reflect)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub struct PointerInteraction {
|
||||||
|
pub(crate) sorted_entities: Vec<(Entity, HitData)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource that maps each [`PointerId`] to their [`Entity`] for easy lookups.
|
||||||
|
#[derive(Debug, Clone, Default, Resource)]
|
||||||
|
pub struct PointerMap {
|
||||||
|
inner: HashMap<PointerId, Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerMap {
|
||||||
|
/// Get the [`Entity`] of the supplied [`PointerId`].
|
||||||
|
pub fn get_entity(&self, pointer_id: PointerId) -> Option<Entity> {
|
||||||
|
self.inner.get(&pointer_id).copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the [`PointerMap`] resource with the current frame's data.
|
||||||
|
pub fn update_pointer_map(pointers: Query<(Entity, &PointerId)>, mut map: ResMut<PointerMap>) {
|
||||||
|
map.inner.clear();
|
||||||
|
for (entity, id) in &pointers {
|
||||||
|
map.inner.insert(*id, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks the state of the pointer's buttons in response to [`InputPress`]s.
|
||||||
|
#[derive(Debug, Default, Clone, Component, Reflect, PartialEq, Eq)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub struct PointerPress {
|
||||||
|
primary: bool,
|
||||||
|
secondary: bool,
|
||||||
|
middle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerPress {
|
||||||
|
/// Returns true if the primary pointer button is pressed.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_primary_pressed(&self) -> bool {
|
||||||
|
self.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the secondary pointer button is pressed.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_secondary_pressed(&self) -> bool {
|
||||||
|
self.secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the middle (tertiary) pointer button is pressed.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_middle_pressed(&self) -> bool {
|
||||||
|
self.middle
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if any pointer button is pressed.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_any_pressed(&self) -> bool {
|
||||||
|
self.primary || self.middle || self.secondary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer input event for button presses. Fires when a pointer button changes state.
|
||||||
|
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
|
||||||
|
pub struct InputPress {
|
||||||
|
/// The [`PointerId`] of the pointer that pressed a button.
|
||||||
|
pub pointer_id: PointerId,
|
||||||
|
/// Direction of the button press.
|
||||||
|
pub direction: PressDirection,
|
||||||
|
/// Identifies the pointer button changing in this event.
|
||||||
|
pub button: PointerButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputPress {
|
||||||
|
/// Create a new pointer button down event.
|
||||||
|
pub fn new_down(id: PointerId, button: PointerButton) -> InputPress {
|
||||||
|
Self {
|
||||||
|
pointer_id: id,
|
||||||
|
direction: PressDirection::Down,
|
||||||
|
button,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new pointer button up event.
|
||||||
|
pub fn new_up(id: PointerId, button: PointerButton) -> InputPress {
|
||||||
|
Self {
|
||||||
|
pointer_id: id,
|
||||||
|
direction: PressDirection::Up,
|
||||||
|
button,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the `button` of this pointer was just pressed.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_just_down(&self, button: PointerButton) -> bool {
|
||||||
|
self.button == button && self.direction == PressDirection::Down
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the `button` of this pointer was just released.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_just_up(&self, button: PointerButton) -> bool {
|
||||||
|
self.button == button && self.direction == PressDirection::Up
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives [`InputPress`] events and updates corresponding [`PointerPress`] components.
|
||||||
|
pub fn receive(
|
||||||
|
mut events: EventReader<InputPress>,
|
||||||
|
mut pointers: Query<(&PointerId, &mut PointerPress)>,
|
||||||
|
) {
|
||||||
|
for input_press_event in events.read() {
|
||||||
|
pointers.iter_mut().for_each(|(pointer_id, mut pointer)| {
|
||||||
|
if *pointer_id == input_press_event.pointer_id {
|
||||||
|
let is_down = input_press_event.direction == PressDirection::Down;
|
||||||
|
match input_press_event.button {
|
||||||
|
PointerButton::Primary => pointer.primary = is_down,
|
||||||
|
PointerButton::Secondary => pointer.secondary = is_down,
|
||||||
|
PointerButton::Middle => pointer.middle = is_down,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The stage of the pointer button press event
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)]
|
||||||
|
pub enum PressDirection {
|
||||||
|
/// The pointer button was just pressed
|
||||||
|
Down,
|
||||||
|
/// The pointer button was just released
|
||||||
|
Up,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The button that was just pressed or released
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
|
||||||
|
pub enum PointerButton {
|
||||||
|
/// The primary pointer button
|
||||||
|
Primary,
|
||||||
|
/// The secondary pointer button
|
||||||
|
Secondary,
|
||||||
|
/// The tertiary pointer button
|
||||||
|
Middle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerButton {
|
||||||
|
/// Iterator over all buttons that a pointer can have.
|
||||||
|
pub fn iter() -> impl Iterator<Item = PointerButton> {
|
||||||
|
[Self::Primary, Self::Secondary, Self::Middle].into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component that tracks a pointer's current [`Location`].
|
||||||
|
#[derive(Debug, Default, Clone, Component, Reflect, PartialEq)]
|
||||||
|
#[reflect(Component, Default)]
|
||||||
|
pub struct PointerLocation {
|
||||||
|
/// The [`Location`] of the pointer. Note that a location is both the target, and the position
|
||||||
|
/// on the target.
|
||||||
|
#[reflect(ignore)]
|
||||||
|
pub location: Option<Location>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PointerLocation {
|
||||||
|
/// Returns `Some(&`[`Location`]`)` if the pointer is active, or `None` if the pointer is
|
||||||
|
/// inactive.
|
||||||
|
pub fn location(&self) -> Option<&Location> {
|
||||||
|
self.location.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pointer input event for pointer moves. Fires when a pointer changes location.
|
||||||
|
#[derive(Event, Debug, Clone, Reflect)]
|
||||||
|
pub struct InputMove {
|
||||||
|
/// The [`PointerId`] of the pointer that is moving.
|
||||||
|
pub pointer_id: PointerId,
|
||||||
|
/// The [`Location`] of the pointer.
|
||||||
|
pub location: Location,
|
||||||
|
/// The distance moved (change in `position`) since the last event.
|
||||||
|
pub delta: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputMove {
|
||||||
|
/// Create a new [`InputMove`] event.
|
||||||
|
pub fn new(id: PointerId, location: Location, delta: Vec2) -> InputMove {
|
||||||
|
Self {
|
||||||
|
pointer_id: id,
|
||||||
|
location,
|
||||||
|
delta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Receives [`InputMove`] events and updates corresponding [`PointerLocation`] components.
|
||||||
|
pub fn receive(
|
||||||
|
mut events: EventReader<InputMove>,
|
||||||
|
mut pointers: Query<(&PointerId, &mut PointerLocation)>,
|
||||||
|
) {
|
||||||
|
for event_pointer in events.read() {
|
||||||
|
pointers.iter_mut().for_each(|(id, mut pointer)| {
|
||||||
|
if *id == event_pointer.pointer_id {
|
||||||
|
pointer.location = Some(event_pointer.location.to_owned());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The location of a pointer, including the current [`NormalizedRenderTarget`], and the x/y
|
||||||
|
/// position of the pointer on this render target.
|
||||||
|
///
|
||||||
|
/// Note that:
|
||||||
|
/// - a pointer can move freely between render targets
|
||||||
|
/// - a pointer is not associated with a [`Camera`] because multiple cameras can target the same
|
||||||
|
/// render target. It is up to picking backends to associate a Pointer's `Location` with a
|
||||||
|
/// specific `Camera`, if any.
|
||||||
|
#[derive(Debug, Clone, Component, Reflect, PartialEq)]
|
||||||
|
pub struct Location {
|
||||||
|
/// The [`NormalizedRenderTarget`] associated with the pointer, usually a window.
|
||||||
|
pub target: NormalizedRenderTarget,
|
||||||
|
/// The position of the pointer in the `target`.
|
||||||
|
pub position: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Location {
|
||||||
|
/// Returns `true` if this pointer's [`Location`] is within the [`Camera`]'s viewport.
|
||||||
|
///
|
||||||
|
/// Note this returns `false` if the location and camera have different render targets.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_in_viewport(
|
||||||
|
&self,
|
||||||
|
camera: &Camera,
|
||||||
|
primary_window: &Query<Entity, With<PrimaryWindow>>,
|
||||||
|
) -> bool {
|
||||||
|
if camera
|
||||||
|
.target
|
||||||
|
.normalize(Some(match primary_window.get_single() {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(_) => return false,
|
||||||
|
}))
|
||||||
|
.as_ref()
|
||||||
|
!= Some(&self.target)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = Vec2::new(self.position.x, self.position.y);
|
||||||
|
|
||||||
|
camera
|
||||||
|
.logical_viewport_rect()
|
||||||
|
.map(|Rect { min, max }| {
|
||||||
|
(position - min).min_element() >= 0.0 && (position - max).max_element() <= 0.0
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ The default feature set enables most of the expected features of a game engine,
|
||||||
|bevy_gizmos|Adds support for rendering gizmos|
|
|bevy_gizmos|Adds support for rendering gizmos|
|
||||||
|bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|
|
|bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|
|
||||||
|bevy_pbr|Adds PBR rendering|
|
|bevy_pbr|Adds PBR rendering|
|
||||||
|
|bevy_picking|Provides picking functionality|
|
||||||
|bevy_render|Provides rendering functionality|
|
|bevy_render|Provides rendering functionality|
|
||||||
|bevy_scene|Provides scene functionality|
|
|bevy_scene|Provides scene functionality|
|
||||||
|bevy_sprite|Provides sprite functionality|
|
|bevy_sprite|Provides sprite functionality|
|
||||||
|
|
|
@ -47,6 +47,7 @@ crates=(
|
||||||
bevy_internal
|
bevy_internal
|
||||||
bevy_dylib
|
bevy_dylib
|
||||||
bevy_color
|
bevy_color
|
||||||
|
bevy_picking
|
||||||
)
|
)
|
||||||
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
if [ -n "$(git status --porcelain)" ]; then
|
||||||
|
|
Loading…
Reference in a new issue