From a04ebb84266c817ba9be175eae516fd2c337c506 Mon Sep 17 00:00:00 2001 From: Schmarni Date: Wed, 20 Nov 2024 11:46:24 +0100 Subject: [PATCH] add XrTracker component that auto parents the entity to the XrTrackingRoot, XrSpace requires XrTracker Signed-off-by: Schmarni --- crates/bevy_openxr/examples/raw_actions.rs | 35 ++++----- crates/bevy_openxr/examples/tracking_utils.rs | 72 +++++++++---------- .../src/openxr/features/handtracking.rs | 16 ++--- crates/bevy_openxr/src/openxr/render.rs | 38 +++------- crates/bevy_xr/src/camera.rs | 5 +- crates/bevy_xr/src/hands.rs | 40 ++++------- crates/bevy_xr/src/session.rs | 27 ++++++- crates/bevy_xr/src/spaces.rs | 34 ++------- crates/bevy_xr_utils/src/hand_gizmos.rs | 4 +- crates/bevy_xr_utils/src/tracking_utils.rs | 60 +++++----------- 10 files changed, 128 insertions(+), 203 deletions(-) diff --git a/crates/bevy_openxr/examples/raw_actions.rs b/crates/bevy_openxr/examples/raw_actions.rs index c706a0e..6673771 100644 --- a/crates/bevy_openxr/examples/raw_actions.rs +++ b/crates/bevy_openxr/examples/raw_actions.rs @@ -11,7 +11,7 @@ use bevy_mod_openxr::{ spaces::OxrSpaceExt, }; use bevy_mod_xr::{ - session::{session_available, session_running, XrSessionCreated, XrTrackingRoot}, + session::{session_available, session_running, XrSessionCreated}, spaces::XrSpace, }; use openxr::Posef; @@ -108,7 +108,6 @@ fn create_actions(instance: Res, mut cmds: Commands) { fn spawn_hands( actions: Res, mut cmds: Commands, - root: Query>, session: Res, mut meshes: ResMut>, mut materials: ResMut>, @@ -128,26 +127,18 @@ fn spawn_hands( let right_space = session .create_action_space(&actions.right, openxr::Path::NULL, Isometry3d::IDENTITY) .unwrap(); - let left = cmds - .spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), - MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), - Transform::from_xyz(0.0, 0.5, 0.0), - left_space, - Controller, - )) - .id(); - let right = cmds - .spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), - MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), - Transform::from_xyz(0.0, 0.5, 0.0), - right_space, - Controller, - )) - .id(); - - cmds.entity(root.single()).add_children(&[left, right]); + cmds.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), + MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + left_space, + Controller, + )); + cmds.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), + MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + right_space, + Controller, + )); } #[derive(Component)] diff --git a/crates/bevy_openxr/examples/tracking_utils.rs b/crates/bevy_openxr/examples/tracking_utils.rs index 7e9cf82..41be029 100644 --- a/crates/bevy_openxr/examples/tracking_utils.rs +++ b/crates/bevy_openxr/examples/tracking_utils.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use bevy_mod_openxr::add_xr_plugins; -use bevy_mod_xr::session::XrSessionCreated; +use bevy_mod_xr::session::{XrSessionCreated, XrTracker}; use bevy_xr_utils::tracking_utils::{ TrackingUtilitiesPlugin, XrTrackedLeftGrip, XrTrackedLocalFloor, XrTrackedRightGrip, XrTrackedStage, XrTrackedView, @@ -53,50 +53,42 @@ fn spawn_hands( mut meshes: ResMut>, mut materials: ResMut>, ) { - let left = cmds - .spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), - MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), - Transform::from_xyz(0.0, 0.5, 0.0), - XrTrackedLeftGrip, - )) - .id(); - let bundle = ( + cmds.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), + MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + Transform::from_xyz(0.0, 0.5, 0.0), + XrTrackedLeftGrip, + XrTracker, + )); + cmds.spawn(( Mesh3d(meshes.add(Cuboid::new(0.1, 0.1, 0.05))), MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), Transform::from_xyz(0.0, 0.5, 0.0), XrTrackedRightGrip, - ); - let right = cmds.spawn(bundle).id(); + XrTracker, + )); //head - - let head = cmds - .spawn(( - Mesh3d(meshes.add(Cuboid::new(0.2, 0.2, 0.2))), - MeshMaterial3d(materials.add(Color::srgb_u8(255, 144, 144))), - Transform::from_xyz(0.0, 0.0, 0.0), - XrTrackedView, - )) - .id(); + cmds.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.2, 0.2, 0.2))), + MeshMaterial3d(materials.add(Color::srgb_u8(255, 144, 144))), + Transform::from_xyz(0.0, 0.0, 0.0), + XrTrackedView, + XrTracker, + )); //local_floor emulated - let local_floor = cmds - .spawn(( - Mesh3d(meshes.add(Cuboid::new(0.5, 0.1, 0.5))), - MeshMaterial3d(materials.add(Color::srgb_u8(144, 255, 144))), - Transform::from_xyz(0.0, 0.0, 0.0), - XrTrackedLocalFloor, - )) - .id(); + cmds.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.5, 0.1, 0.5))), + MeshMaterial3d(materials.add(Color::srgb_u8(144, 255, 144))), + Transform::from_xyz(0.0, 0.0, 0.0), + XrTrackedLocalFloor, + XrTracker, + )); - let stage = cmds - .spawn(( - Mesh3d(meshes.add(Cuboid::new(0.5, 0.1, 0.5))), - MeshMaterial3d(materials.add(Color::srgb_u8(144, 255, 255))), - Transform::from_xyz(0.0, 0.0, 0.0), - XrTrackedStage, - )) - .id(); - - cmds.entity(stage) - .add_children(&[left, right, head, local_floor]); + cmds.spawn(( + Mesh3d(meshes.add(Cuboid::new(0.5, 0.1, 0.5))), + MeshMaterial3d(materials.add(Color::srgb_u8(144, 255, 255))), + Transform::from_xyz(0.0, 0.0, 0.0), + XrTrackedStage, + XrTracker, + )); } diff --git a/crates/bevy_openxr/src/openxr/features/handtracking.rs b/crates/bevy_openxr/src/openxr/features/handtracking.rs index 702ed19..65a57b6 100644 --- a/crates/bevy_openxr/src/openxr/features/handtracking.rs +++ b/crates/bevy_openxr/src/openxr/features/handtracking.rs @@ -1,10 +1,10 @@ use bevy::prelude::*; use bevy_mod_xr::hands::{ - spawn_hand_bones, HandBone, HandBoneRadius, HandSide, SpawnHandTracker, - SpawnHandTrackerCommandExecutor, + spawn_hand_bones, HandBone, HandSide, SpawnHandTracker, SpawnHandTrackerCommandExecutor, + XrHandBoneRadius, }; use bevy_mod_xr::hands::{LeftHand, RightHand, XrHandBoneEntities}; -use bevy_mod_xr::session::{XrPreDestroySession, XrSessionCreated, XrTrackingRoot}; +use bevy_mod_xr::session::{XrPreDestroySession, XrSessionCreated}; use bevy_mod_xr::spaces::{ XrPrimaryReferenceSpace, XrReferenceSpace, XrSpaceLocationFlags, XrSpaceVelocityFlags, XrVelocity, @@ -70,11 +70,7 @@ fn handle_tracker_spawn(world: &mut World, tracker: Entity, side: HandSide) { .insert(OxrHandTracker(oxr_tracker)); } -fn spawn_default_hands(mut cmds: Commands, root: Query>) { - let Ok(root) = root.get_single() else { - error!("unable to get tracking root, skipping handtracker creation"); - return; - }; +fn spawn_default_hands(mut cmds: Commands) { debug!("spawning default hands"); let left_bones = spawn_hand_bones(&mut cmds, |_| { ( @@ -90,8 +86,6 @@ fn spawn_default_hands(mut cmds: Commands, root: Query, mut bone_query: Query<( &HandBone, - &mut HandBoneRadius, + &mut XrHandBoneRadius, &mut Transform, Option<&mut XrVelocity>, &mut OxrSpaceLocationFlags, diff --git a/crates/bevy_openxr/src/openxr/render.rs b/crates/bevy_openxr/src/openxr/render.rs index b226724..0fb0269 100644 --- a/crates/bevy_openxr/src/openxr/render.rs +++ b/crates/bevy_openxr/src/openxr/render.rs @@ -1,5 +1,4 @@ use bevy::{ - ecs::query::QuerySingleError, prelude::*, render::{ camera::{ManualTextureView, ManualTextureViewHandle, ManualTextureViews, RenderTarget}, @@ -12,9 +11,7 @@ use bevy::{ }; use bevy_mod_xr::{ camera::{XrCamera, XrProjection}, - session::{ - XrFirst, XrHandleEvents, XrPreDestroySession, XrRenderSet, XrRootTransform, XrTrackingRoot, - }, + session::{XrFirst, XrHandleEvents, XrPreDestroySession, XrRenderSet, XrRootTransform}, spaces::XrPrimaryReferenceSpace, }; use openxr::ViewStateFlags; @@ -34,10 +31,6 @@ impl Plugin for OxrRenderPlugin { fn build(&self, app: &mut App) { if app.is_plugin_added::() { app.init_resource::(); - - // if let Some(sub_app) = app.remove_sub_app(RenderExtractApp) { - // app.insert_sub_app(RenderExtractApp, SubApp::new(sub_app.app, update_rendering)); - // } } app.add_plugins(( @@ -140,7 +133,6 @@ pub fn init_views( graphics_info: Res, mut manual_texture_views: ResMut, swapchain_images: Res, - root: Query>, mut commands: Commands, ) { let _span = info_span!("xr_init_views"); @@ -150,27 +142,13 @@ pub fn init_views( info!("XrCamera resolution: {}", graphics_info.resolution); let view_handle = add_texture_view(&mut manual_texture_views, temp_tex, &graphics_info, index); - let cam = commands - .spawn(( - Camera { - target: RenderTarget::TextureView(view_handle), - ..Default::default() - }, - XrCamera(index), - )) - .remove::() - .id(); - match root.get_single() { - Ok(root) => { - commands.entity(root).add_child(cam); - } - Err(QuerySingleError::NoEntities(_)) => { - warn!("No XrTrackingRoot!"); - } - Err(QuerySingleError::MultipleEntities(_)) => { - warn!("Multiple XrTrackingRoots! this is not allowed"); - } - } + commands.spawn(( + Camera { + target: RenderTarget::TextureView(view_handle), + ..Default::default() + }, + XrCamera(index), + )); } } diff --git a/crates/bevy_xr/src/camera.rs b/crates/bevy_xr/src/camera.rs index 562faf0..b779f51 100644 --- a/crates/bevy_xr/src/camera.rs +++ b/crates/bevy_xr/src/camera.rs @@ -1,7 +1,6 @@ use core::panic; use bevy::app::{App, Plugin, PostUpdate}; -use bevy::core_pipeline::core_3d::graph::Core3d; use bevy::core_pipeline::core_3d::Camera3d; use bevy::ecs::component::{Component, StorageType}; use bevy::ecs::reflect::ReflectComponent; @@ -18,6 +17,8 @@ use bevy::render::extract_component::{ExtractComponent, ExtractComponentPlugin}; use bevy::render::view::{update_frusta, VisibilitySystems}; use bevy::transform::TransformSystem; +use crate::session::XrTracker; + pub struct XrCameraPlugin; impl Plugin for XrCameraPlugin { @@ -69,7 +70,7 @@ impl Default for XrProjection { /// Marker component for an XR view. It is the backends responsibility to update this. #[derive(Clone, Copy, Component, ExtractComponent, Debug, Default)] -#[require(Camera3d, XrProjection)] +#[require(Camera3d, XrProjection, XrTracker)] pub struct XrCamera(pub u32); impl CameraProjection for XrProjection { diff --git a/crates/bevy_xr/src/hands.rs b/crates/bevy_xr/src/hands.rs index d5e9a68..0bcc497 100644 --- a/crates/bevy_xr/src/hands.rs +++ b/crates/bevy_xr/src/hands.rs @@ -1,11 +1,11 @@ use bevy::{ ecs::{component::Component, entity::Entity, world::Command}, - log::{error, warn}, + log::warn, math::bool, - prelude::{BuildChildren, Bundle, Commands, Deref, DerefMut, Resource, Transform, Visibility, With, World}, + prelude::{Bundle, Commands, Deref, DerefMut, Resource, Transform, Visibility, World}, }; -use crate::{session::XrTrackingRoot, spaces::XrSpaceLocationFlags}; +use crate::{session::XrTracker, spaces::XrSpaceLocationFlags}; pub const HAND_JOINT_COUNT: usize = 26; pub fn spawn_hand_bones( @@ -14,16 +14,7 @@ pub fn spawn_hand_bones( ) -> [Entity; HAND_JOINT_COUNT] { let mut bones: [Entity; HAND_JOINT_COUNT] = [Entity::PLACEHOLDER; HAND_JOINT_COUNT]; for bone in HandBone::get_all_bones().into_iter() { - bones[bone as usize] = cmds - .spawn(( - Transform::default(), - Visibility::default(), - bone, - HandBoneRadius(0.0), - XrSpaceLocationFlags::default(), - )) - .insert((get_bundle)(bone)) - .id(); + bones[bone as usize] = cmds.spawn((bone, (get_bundle)(bone))).id(); } bones } @@ -45,11 +36,18 @@ pub struct RightHand; pub struct XrHandBoneEntities(pub [Entity; HAND_JOINT_COUNT]); #[repr(transparent)] -#[derive(Clone, Copy, Component, Debug, DerefMut, Deref)] -pub struct HandBoneRadius(pub f32); +#[derive(Clone, Copy, Component, Debug, DerefMut, Deref, Default)] +pub struct XrHandBoneRadius(pub f32); #[repr(u8)] #[derive(Clone, Copy, Component, Debug)] +#[require( + XrSpaceLocationFlags, + XrHandBoneRadius, + Transform, + Visibility, + XrTracker +)] pub enum HandBone { Palm = 0, Wrist = 1, @@ -189,20 +187,12 @@ impl Command for SpawnHandTracker { warn!("no SpawnHandTracker executor defined, skipping handtracker creation"); return; }; - let Ok(root) = world - .query_filtered::>() - .get_single(world) - else { - error!("unable to get tracking root, skipping handtracker creation"); - return; - }; let mut tracker = world.spawn(self.joints); match &self.side { - HandSide::Left => tracker.insert(LeftHand), - HandSide::Right => tracker.insert(LeftHand), + HandSide::Left => tracker.insert((XrTracker, LeftHand)), + HandSide::Right => tracker.insert((XrTracker, RightHand)), }; let tracker = tracker.id(); - world.entity_mut(root).add_children(&[tracker]); executor.0(world, tracker, self.side); if let Ok(mut tracker) = world.get_entity_mut(tracker) { tracker.insert(self.side); diff --git a/crates/bevy_xr/src/session.rs b/crates/bevy_xr/src/session.rs index 64d1aad..ba0ced6 100644 --- a/crates/bevy_xr/src/session.rs +++ b/crates/bevy_xr/src/session.rs @@ -2,6 +2,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use bevy::app::{AppExit, MainScheduleOrder}; +use bevy::ecs::component::StorageType; use bevy::ecs::schedule::ScheduleLabel; use bevy::prelude::*; use bevy::render::extract_resource::{ExtractResource, ExtractResourcePlugin}; @@ -84,7 +85,29 @@ pub struct XrRootTransform(pub GlobalTransform); /// Component used to specify the entity we should use as the tracking root. #[derive(Component)] +#[require(Transform, Visibility)] pub struct XrTrackingRoot; +#[derive(Resource)] +struct TrackingRootRes(Entity); + +/// Makes the entity a child of the XrTrackingRoot if the entity has no parent +#[derive(Clone, Copy, Hash, PartialEq, Eq, Reflect, Debug, Default)] +pub struct XrTracker; +impl Component for XrTracker { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + + fn register_component_hooks(hooks: &mut bevy::ecs::component::ComponentHooks) { + hooks.on_add(|mut world, entity, _| { + if world.entity(entity).components::>() { + return; + } + let Some(root) = world.get_resource::().map(|r| r.0) else { + return; + }; + world.commands().entity(root).add_child(entity); + }); + } +} pub struct XrSessionPlugin { pub auto_handle: bool, @@ -125,12 +148,12 @@ impl Plugin for XrSessionPlugin { .run_if(session_created) .in_set(XrHandleEvents::ExitEvents), ); + let root = app.world_mut().spawn(XrTrackingRoot).id(); + app.world_mut().insert_resource(TrackingRootRes(root)); app.world_mut() .resource_mut::() .labels .insert(0, XrFirst.intern()); - app.world_mut() - .spawn((Transform::default(), Visibility::default(), XrTrackingRoot)); if self.auto_handle { app.add_systems(PreUpdate, auto_handle_session); diff --git a/crates/bevy_xr/src/spaces.rs b/crates/bevy_xr/src/spaces.rs index 4f6a295..5e1e4f0 100644 --- a/crates/bevy_xr/src/spaces.rs +++ b/crates/bevy_xr/src/spaces.rs @@ -1,15 +1,18 @@ use bevy::{ - ecs::component::StorageType, prelude::*, render::{extract_component::ExtractComponent, extract_resource::ExtractResource}, }; +use crate::session::XrTracker; + /// Any Spaces will be invalid after the owning session exits #[repr(transparent)] -#[derive(Clone, Copy, Hash, PartialEq, Eq, Reflect, Debug, ExtractComponent)] +#[derive(Component, Clone, Copy, Hash, PartialEq, Eq, Reflect, Debug, ExtractComponent)] +#[require(XrSpaceLocationFlags, Transform, Visibility, XrTracker)] pub struct XrSpace(u64); -#[derive(Clone, Copy, Reflect, Debug, ExtractComponent, Default)] +#[derive(Component, Clone, Copy, Reflect, Debug, ExtractComponent, Default)] +#[require(XrSpaceVelocityFlags)] pub struct XrVelocity { /// Velocity of a space relative to it's reference space pub linear: Vec3, @@ -69,28 +72,3 @@ impl XrSpace { self.0 } } - -impl Component for XrSpace { - const STORAGE_TYPE: StorageType = StorageType::Table; - - fn register_component_hooks(hooks: &mut bevy::ecs::component::ComponentHooks) { - hooks.on_add(|mut world, entity, _| { - world - .commands() - .entity(entity) - .insert(XrSpaceLocationFlags::default()); - }); - } -} -impl Component for XrVelocity { - const STORAGE_TYPE: StorageType = StorageType::Table; - - fn register_component_hooks(hooks: &mut bevy::ecs::component::ComponentHooks) { - hooks.on_add(|mut world, entity, _| { - world - .commands() - .entity(entity) - .insert(XrSpaceVelocityFlags::default()); - }); - } -} diff --git a/crates/bevy_xr_utils/src/hand_gizmos.rs b/crates/bevy_xr_utils/src/hand_gizmos.rs index 6809c0b..1c270b6 100644 --- a/crates/bevy_xr_utils/src/hand_gizmos.rs +++ b/crates/bevy_xr_utils/src/hand_gizmos.rs @@ -1,6 +1,6 @@ use bevy::color::palettes::css; use bevy::{prelude::*, transform::TransformSystem}; -use bevy_mod_xr::hands::{HandBone, HandBoneRadius}; +use bevy_mod_xr::hands::{HandBone, XrHandBoneRadius}; use bevy_mod_xr::spaces::XrSpaceLocationFlags; pub struct HandGizmosPlugin; impl Plugin for HandGizmosPlugin { @@ -16,7 +16,7 @@ fn draw_hand_gizmos( query: Query<( &GlobalTransform, &HandBone, - &HandBoneRadius, + &XrHandBoneRadius, &XrSpaceLocationFlags, )>, ) { diff --git a/crates/bevy_xr_utils/src/tracking_utils.rs b/crates/bevy_xr_utils/src/tracking_utils.rs index ad808bc..3e8b1ce 100644 --- a/crates/bevy_xr_utils/src/tracking_utils.rs +++ b/crates/bevy_xr_utils/src/tracking_utils.rs @@ -10,7 +10,7 @@ use bevy_mod_openxr::{ spaces::{OxrSpaceLocationFlags, OxrSpaceSyncSet}, }; use bevy_mod_xr::{ - session::{XrSessionCreated, XrTrackingRoot}, + session::{XrSessionCreated, XrTracker, XrTrackingRoot}, spaces::{XrPrimaryReferenceSpace, XrReferenceSpace}, }; use openxr::Posef; @@ -80,11 +80,10 @@ impl Plugin for TrackingUtilitiesPlugin { //stage fn update_stage( - mut root_query: Query<&mut Transform, (With, Without)>, + root_query: Query<&Transform, (With, Without)>, mut stage_query: Query<&mut Transform, (With, Without)>, ) { - let tracking_root_transform = root_query.get_single_mut(); - if let Ok(root) = tracking_root_transform { + if let Ok(root) = root_query.get_single() { for mut transform in &mut stage_query { *transform = *root; } @@ -148,10 +147,10 @@ fn update_local_floor_transforms( let mut calc_floor = *head; calc_floor.translation.y = 0.0; //TODO: use yaw - let (y, x, z) = calc_floor.rotation.to_euler(EulerRot::YXZ); + let (y, _, _) = calc_floor.rotation.to_euler(EulerRot::YXZ); let new_rot = Quat::from_rotation_y(y); calc_floor.rotation = new_rot; - for (mut transform) in &mut local_floor { + for mut transform in &mut local_floor { *transform = calc_floor; } } @@ -166,13 +165,10 @@ fn update_left_grip( mut tracked_left_grip: Query<&mut Transform, (With, Without)>, ) { let head_transform = left_grip.get_single_mut(); - match head_transform { - Ok(head) => { - for (mut transform) in &mut tracked_left_grip { - *transform = head.clone(); - } + if let Ok(head) = head_transform { + for mut transform in &mut tracked_left_grip { + *transform = *head; } - Err(_) => (), } } @@ -185,13 +181,10 @@ fn update_right_grip( mut tracked_right_grip: Query<&mut Transform, (With, Without)>, ) { let head_transform = right_grip.get_single_mut(); - match head_transform { - Ok(head) => { - for (mut transform) in &mut tracked_right_grip { - *transform = head.clone(); - } + if let Ok(head) = head_transform { + for mut transform in &mut tracked_right_grip { + *transform = *head; } - Err(_) => (), } } @@ -206,16 +199,18 @@ struct ControllerActions { fn spawn_tracking_rig( actions: Res, mut cmds: Commands, - root: Query>, session: Res, ) { //head let head_space = session .create_reference_space(openxr::ReferenceSpaceType::VIEW, Transform::IDENTITY) .unwrap(); - let head = cmds - .spawn((SpatialBundle::default(), HeadXRSpace(head_space))) - .id(); + cmds.spawn(( + Transform::default(), + Visibility::default(), + XrTracker, + HeadXRSpace(head_space), + )); // let local_floor = cmds.spawn((SpatialBundle::default(), LocalFloor)).id(); let left_space = session @@ -224,25 +219,8 @@ fn spawn_tracking_rig( let right_space = session .create_action_space(&actions.right, openxr::Path::NULL, Isometry3d::IDENTITY) .unwrap(); - let left = cmds - .spawn(( - Transform::default(), - Visibility::default(), - left_space, - LeftGrip, - )) - .id(); - let right = cmds - .spawn(( - Transform::default(), - Visibility::default(), - right_space, - RightGrip, - )) - .id(); - - cmds.entity(root.single()) - .add_children(&[head, left, right]); + cmds.spawn((left_space, LeftGrip)); + cmds.spawn((right_space, RightGrip)); } //bindings