diff --git a/crates/bevy_openxr/examples/tracking_utils.rs b/crates/bevy_openxr/examples/tracking_utils.rs new file mode 100644 index 0000000..7f79a54 --- /dev/null +++ b/crates/bevy_openxr/examples/tracking_utils.rs @@ -0,0 +1,119 @@ +//! A simple 3D scene with light shining over a cube sitting on a plane. + +use bevy::prelude::*; +use bevy_mod_openxr::add_xr_plugins; +use bevy_mod_xr::session::{XrSessionCreated, XrTrackingRoot}; +use bevy_xr_utils::tracking_utils::{ + TrackingUtilitiesPlugin, XrTrackedLeftGrip, XrTrackedLocalFloor, XrTrackedRightGrip, + XrTrackedStage, XrTrackedView, +}; + +fn main() { + let mut app = App::new(); + app.add_plugins(add_xr_plugins(DefaultPlugins)); + app.add_systems(Startup, setup); + + //things? + app.add_systems(XrSessionCreated, spawn_hands); + + //tracking utils plugin + app.add_plugins(TrackingUtilitiesPlugin); + + app.run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // circular base + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + ..default() + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +} + +fn spawn_hands( + mut cmds: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let left = cmds + .spawn(( + PbrBundle { + mesh: meshes.add(Cuboid::new(0.1, 0.1, 0.05)), + material: materials.add(Color::srgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }, + XrTrackedLeftGrip, + )) + .id(); + let bundle = ( + PbrBundle { + mesh: meshes.add(Cuboid::new(0.1, 0.1, 0.05)), + material: materials.add(Color::srgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }, + XrTrackedRightGrip, + ); + let right = cmds.spawn(bundle).id(); + //head + + let head = cmds + .spawn(( + PbrBundle { + mesh: meshes.add(Cuboid::new(0.2, 0.2, 0.2)), + material: materials.add(Color::srgb_u8(255, 144, 144)), + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..default() + }, + XrTrackedView, + )) + .id(); + //local_floor emulated + let local_floor = cmds + .spawn(( + PbrBundle { + mesh: meshes.add(Cuboid::new(0.5, 0.1, 0.5)), + material: materials.add(Color::srgb_u8(144, 255, 144)), + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..default() + }, + XrTrackedLocalFloor, + )) + .id(); + + let stage = cmds + .spawn(( + PbrBundle { + mesh: meshes.add(Cuboid::new(0.5, 0.1, 0.5)), + material: materials.add(Color::srgb_u8(144, 255, 255)), + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..default() + }, + XrTrackedStage, + )) + .id(); + + cmds.entity(stage) + .push_children(&[left, right, head, local_floor]); +} diff --git a/crates/bevy_xr_utils/src/lib.rs b/crates/bevy_xr_utils/src/lib.rs index df9edfd..13f8460 100644 --- a/crates/bevy_xr_utils/src/lib.rs +++ b/crates/bevy_xr_utils/src/lib.rs @@ -1,6 +1,8 @@ // use bevy::prelude::*; pub mod hand_gizmos; #[cfg(not(target_family = "wasm"))] -pub mod xr_utils_actions; +pub mod tracking_utils; #[cfg(not(target_family = "wasm"))] pub mod transform_utils; +#[cfg(not(target_family = "wasm"))] +pub mod xr_utils_actions; diff --git a/crates/bevy_xr_utils/src/tracking_utils.rs b/crates/bevy_xr_utils/src/tracking_utils.rs new file mode 100644 index 0000000..cd75dce --- /dev/null +++ b/crates/bevy_xr_utils/src/tracking_utils.rs @@ -0,0 +1,283 @@ +use bevy::prelude::*; +use bevy_mod_openxr::{ + action_binding::{OxrSendActionBindings, OxrSuggestActionBinding}, + action_set_attaching::OxrAttachActionSet, + action_set_syncing::{OxrActionSetSyncSet, OxrSyncActionSet}, + helper_traits::{ToQuat, ToVec3}, + resources::{OxrFrameState, OxrInstance, Pipelined}, + session::OxrSession, + spaces::{OxrSpaceLocationFlags, OxrSpaceSyncSet}, +}; +use bevy_mod_xr::{ + session::{session_available, session_running, XrSessionCreated, XrTrackingRoot}, + spaces::{XrPrimaryReferenceSpace, XrReferenceSpace}, + types::XrPose, +}; +use openxr::Posef; + +//exernal api +#[derive(Component)] +pub struct XrTrackedStage; + +#[derive(Component)] +pub struct XrTrackedLocalFloor; + +#[derive(Component)] +pub struct XrTrackedView; + +#[derive(Component)] +pub struct XrTrackedLeftGrip; + +#[derive(Component)] +pub struct XrTrackedRightGrip; + +pub struct TrackingUtilitiesPlugin; + +impl Plugin for TrackingUtilitiesPlugin { + fn build(&self, app: &mut App) { + //spawn tracking rig + app.add_systems(XrSessionCreated, spawn_tracking_rig); + + //update stage transforms + //external + app.add_systems(PreUpdate, update_stage); + + //head view transforms + //internal + app.add_systems( + PreUpdate, + update_head_transforms + .in_set(OxrSpaceSyncSet) + .run_if(session_running), + ); + //external + app.add_systems(PreUpdate, update_view.after(update_head_transforms)); + + //local floor transforms + app.add_systems( + PreUpdate, + update_local_floor_transforms.after(update_head_transforms), + ); + + //bindings + app.add_systems(OxrSendActionBindings, suggest_action_bindings); + //sync actions + app.add_systems( + PreUpdate, + sync_actions + .before(OxrActionSetSyncSet) + .run_if(session_running), + ); + //attach sets + app.add_systems(XrSessionCreated, attach_set); + //create actions + app.add_systems(Startup, create_actions.run_if(session_available)); + + app.add_systems(PreUpdate, update_left_grip.after(OxrSpaceSyncSet)); + app.add_systems(PreUpdate, update_right_grip.after(OxrSpaceSyncSet)); + } +} + +//stage +fn update_stage( + mut root_query: Query<&mut Transform, (With, Without)>, + mut stage_query: Query<&mut Transform, (With, Without)>, +) { + let tracking_root_transform = root_query.get_single_mut(); + match tracking_root_transform { + Ok(root) => { + for (mut transform) in &mut stage_query { + *transform = root.clone(); + } + } + Err(_) => (), + } +} + +//view +#[derive(Component)] +struct HeadXRSpace(XrReferenceSpace); + +fn update_head_transforms( + session: Res, + default_ref_space: Res, + pipelined: Option>, + frame_state: Res, + mut query: Query<(&mut Transform, &HeadXRSpace, Option<&XrReferenceSpace>)>, +) { + for (mut transform, space, ref_space) in &mut query { + let ref_space = ref_space.unwrap_or(&default_ref_space); + let time = if pipelined.is_some() { + openxr::Time::from_nanos( + frame_state.predicted_display_time.as_nanos() + + frame_state.predicted_display_period.as_nanos(), + ) + } else { + frame_state.predicted_display_time + }; + let space_location = session.locate_space(&space.0, ref_space, time); + + if let Ok(space_location) = space_location { + let flags = OxrSpaceLocationFlags(space_location.location_flags); + if flags.pos_valid() { + transform.translation = space_location.pose.position.to_vec3(); + } + if flags.rot_valid() { + transform.rotation = space_location.pose.orientation.to_quat(); + } + } + } +} + +fn update_view( + mut head_query: Query<&mut Transform, (With, Without)>, + mut view_query: Query<&mut Transform, (With, Without)>, +) { + let head_transform = head_query.get_single_mut(); + match head_transform { + Ok(root) => { + for (mut transform) in &mut view_query { + *transform = root.clone(); + } + } + Err(_) => (), + } +} + +//local floor +fn update_local_floor_transforms( + mut head_space: Query<&mut Transform, (With, Without)>, + mut local_floor: Query<&mut Transform, (With, Without)>, +) { + let head_transform = head_space.get_single_mut(); + match head_transform { + Ok(head) => { + let mut calc_floor = head.clone(); + calc_floor.translation.y = 0.0; + //TODO: use yaw + let (y, x, z) = 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 { + *transform = calc_floor; + } + } + Err(_) => (), + } +} + +//left grip +#[derive(Component)] +struct LeftGrip; + +fn update_left_grip( + mut left_grip: Query<&mut Transform, (With, Without)>, + 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(); + } + } + Err(_) => (), + } +} + +//right grip +#[derive(Component)] +struct RightGrip; + +fn update_right_grip( + mut right_grip: Query<&mut Transform, (With, Without)>, + 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(); + } + } + Err(_) => (), + } +} + +//tracking rig +#[derive(Resource)] +struct ControllerActions { + set: openxr::ActionSet, + left: openxr::Action, + right: openxr::Action, +} + +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(); + // let local_floor = cmds.spawn((SpatialBundle::default(), LocalFloor)).id(); + + let left_space = session + .create_action_space(&actions.left, openxr::Path::NULL, XrPose::IDENTITY) + .unwrap(); + let right_space = session + .create_action_space(&actions.right, openxr::Path::NULL, XrPose::IDENTITY) + .unwrap(); + let left = cmds + .spawn((SpatialBundle::default(), left_space, LeftGrip)) + .id(); + let right = cmds + .spawn((SpatialBundle::default(), right_space, RightGrip)) + .id(); + + cmds.entity(root.single()) + .push_children(&[head, left, right]); +} + +//bindings +//TODO figure out how to make these better, specifically not be controller specific +fn suggest_action_bindings( + actions: Res, + mut bindings: EventWriter, +) { + bindings.send(OxrSuggestActionBinding { + action: actions.left.as_raw(), + interaction_profile: "/interaction_profiles/oculus/touch_controller".into(), + bindings: vec!["/user/hand/left/input/grip/pose".into()], + }); + bindings.send(OxrSuggestActionBinding { + action: actions.right.as_raw(), + interaction_profile: "/interaction_profiles/oculus/touch_controller".into(), + bindings: vec!["/user/hand/right/input/grip/pose".into()], + }); +} + +fn sync_actions(actions: Res, mut sync: EventWriter) { + sync.send(OxrSyncActionSet(actions.set.clone())); +} + +fn attach_set(actions: Res, mut attach: EventWriter) { + attach.send(OxrAttachActionSet(actions.set.clone())); +} + +fn create_actions(instance: Res, mut cmds: Commands) { + let set = instance.create_action_set("hands", "Hands", 0).unwrap(); + let left = set + .create_action("left_pose", "Left Hand Grip Pose", &[]) + .unwrap(); + let right = set + .create_action("right_pose", "Right Hand Grip Pose", &[]) + .unwrap(); + + cmds.insert_resource(ControllerActions { set, left, right }) +}