diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-01-30 20:07:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-30 19:07:35 +0000 |
commit | 534e78db5331fbfbad7e60d72a88e9aacdc11ee4 (patch) | |
tree | 3bcd217e0e7b7fd0c91d5406a81ea241ffc4ce06 /components/webxr/openxr/input.rs | |
parent | 64b40ea70065f949d1e281bd046c56d50312f2a7 (diff) | |
download | servo-534e78db5331fbfbad7e60d72a88e9aacdc11ee4.tar.gz servo-534e78db5331fbfbad7e60d72a88e9aacdc11ee4.zip |
Merge webxr repository (#35228)
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components/webxr/openxr/input.rs')
-rw-r--r-- | components/webxr/openxr/input.rs | 743 |
1 files changed, 743 insertions, 0 deletions
diff --git a/components/webxr/openxr/input.rs b/components/webxr/openxr/input.rs new file mode 100644 index 00000000000..bcd060f3876 --- /dev/null +++ b/components/webxr/openxr/input.rs @@ -0,0 +1,743 @@ +use std::ffi::c_void; +use std::mem::MaybeUninit; + +use euclid::RigidTransform3D; +use log::debug; +use openxr::sys::{ + HandJointLocationsEXT, HandJointsLocateInfoEXT, HandTrackingAimStateFB, + FB_HAND_TRACKING_AIM_EXTENSION_NAME, +}; +use openxr::{ + self, Action, ActionSet, Binding, FrameState, Graphics, Hand as HandEnum, HandJoint, + HandJointLocation, HandTracker, HandTrackingAimFlagsFB, Instance, Path, Posef, Session, Space, + SpaceLocationFlags, HAND_JOINT_COUNT, +}; +use webxr_api::Finger; +use webxr_api::Hand; +use webxr_api::Handedness; +use webxr_api::Input; +use webxr_api::InputFrame; +use webxr_api::InputId; +use webxr_api::InputSource; +use webxr_api::JointFrame; +use webxr_api::Native; +use webxr_api::SelectEvent; +use webxr_api::TargetRayMode; +use webxr_api::Viewer; + +use super::interaction_profiles::InteractionProfile; +use super::IDENTITY_POSE; + +use crate::ext_string; +use crate::openxr::interaction_profiles::INTERACTION_PROFILES; + +/// Number of frames to wait with the menu gesture before +/// opening the menu. +const MENU_GESTURE_SUSTAIN_THRESHOLD: u8 = 60; + +/// Helper macro for binding action paths in an interaction profile entry +macro_rules! bind_inputs { + ($actions:expr, $paths:expr, $hand:expr, $instance:expr, $ret:expr) => { + $actions.iter().enumerate().for_each(|(i, action)| { + let action_path = $paths[i]; + if action_path != "" { + let path = $instance + .string_to_path(&format!("/user/hand/{}/input/{}", $hand, action_path)) + .expect(&format!( + "Failed to create path for /user/hand/{}/input/{}", + $hand, action_path + )); + let binding = Binding::new(action, path); + $ret.push(binding); + } + }); + }; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum ClickState { + Clicking, + Done, +} + +/// All the information on a single input frame +pub struct Frame { + pub frame: InputFrame, + pub select: Option<SelectEvent>, + pub squeeze: Option<SelectEvent>, + pub menu_selected: bool, +} + +impl ClickState { + fn update_from_action<G: Graphics>( + &mut self, + action: &Action<bool>, + session: &Session<G>, + menu_selected: bool, + ) -> (/* is_active */ bool, Option<SelectEvent>) { + let click = action.state(session, Path::NULL).unwrap(); + + let select_event = + self.update_from_value(click.current_state, click.is_active, menu_selected); + + (click.is_active, select_event) + } + + fn update_from_value( + &mut self, + current_state: bool, + is_active: bool, + menu_selected: bool, + ) -> Option<SelectEvent> { + if is_active { + match (current_state, *self) { + (_, ClickState::Clicking) if menu_selected => { + *self = ClickState::Done; + // Cancel the select, we're showing a menu + Some(SelectEvent::End) + } + (true, ClickState::Done) => { + *self = ClickState::Clicking; + Some(SelectEvent::Start) + } + (false, ClickState::Clicking) => { + *self = ClickState::Done; + Some(SelectEvent::Select) + } + _ => None, + } + } else if *self == ClickState::Clicking { + *self = ClickState::Done; + // Cancel the select, we lost tracking + Some(SelectEvent::End) + } else { + None + } + } +} + +pub struct OpenXRInput { + id: InputId, + action_aim_pose: Action<Posef>, + action_aim_space: Space, + action_grip_pose: Action<Posef>, + action_grip_space: Space, + action_click: Action<bool>, + action_squeeze: Action<bool>, + handedness: Handedness, + click_state: ClickState, + squeeze_state: ClickState, + menu_gesture_sustain: u8, + #[allow(unused)] + hand_tracker: Option<HandTracker>, + action_buttons_common: Vec<Action<f32>>, + action_buttons_left: Vec<Action<f32>>, + action_buttons_right: Vec<Action<f32>>, + action_axes_common: Vec<Action<f32>>, + use_alternate_input_source: bool, +} + +fn hand_str(h: Handedness) -> &'static str { + match h { + Handedness::Right => "right", + Handedness::Left => "left", + _ => panic!("We don't support unknown handedness in openxr"), + } +} + +impl OpenXRInput { + pub fn new<G: Graphics>( + id: InputId, + handedness: Handedness, + action_set: &ActionSet, + session: &Session<G>, + needs_hands: bool, + supported_interaction_profiles: Vec<&'static str>, + ) -> Self { + let hand = hand_str(handedness); + let action_aim_pose: Action<Posef> = action_set + .create_action( + &format!("{}_hand_aim", hand), + &format!("{} hand aim", hand), + &[], + ) + .unwrap(); + let action_aim_space = action_aim_pose + .create_space(session.clone(), Path::NULL, IDENTITY_POSE) + .unwrap(); + let action_grip_pose: Action<Posef> = action_set + .create_action( + &format!("{}_hand_grip", hand), + &format!("{} hand grip", hand), + &[], + ) + .unwrap(); + let action_grip_space = action_grip_pose + .create_space(session.clone(), Path::NULL, IDENTITY_POSE) + .unwrap(); + let action_click: Action<bool> = action_set + .create_action( + &format!("{}_hand_click", hand), + &format!("{} hand click", hand), + &[], + ) + .unwrap(); + let action_squeeze: Action<bool> = action_set + .create_action( + &format!("{}_hand_squeeze", hand), + &format!("{} hand squeeze", hand), + &[], + ) + .unwrap(); + + let hand_tracker = if needs_hands { + let hand = match handedness { + Handedness::Left => HandEnum::LEFT, + Handedness::Right => HandEnum::RIGHT, + _ => panic!("We don't support unknown handedness in openxr"), + }; + session.create_hand_tracker(hand).ok() + } else { + None + }; + + let action_buttons_common: Vec<Action<f32>> = { + let button1: Action<f32> = action_set + .create_action( + &format!("{}_trigger", hand), + &format!("{}_trigger", hand), + &[], + ) + .unwrap(); + let button2: Action<f32> = action_set + .create_action(&format!("{}_grip", hand), &format!("{}_grip", hand), &[]) + .unwrap(); + let button3: Action<f32> = action_set + .create_action( + &format!("{}_touchpad_click", hand), + &format!("{}_touchpad_click", hand), + &[], + ) + .unwrap(); + let button4: Action<f32> = action_set + .create_action( + &format!("{}_thumbstick_click", hand), + &format!("{}_thumbstick_click", hand), + &[], + ) + .unwrap(); + vec![button1, button2, button3, button4] + }; + + let action_buttons_left = { + let button1: Action<f32> = action_set + .create_action(&format!("{}_x", hand), &format!("{}_x", hand), &[]) + .unwrap(); + let button2: Action<f32> = action_set + .create_action(&format!("{}_y", hand), &format!("{}_y", hand), &[]) + .unwrap(); + vec![button1, button2] + }; + + let action_buttons_right = { + let button1: Action<f32> = action_set + .create_action(&format!("{}_a", hand), &format!("{}_a", hand), &[]) + .unwrap(); + let button2: Action<f32> = action_set + .create_action(&format!("{}_b", hand), &format!("{}_b", hand), &[]) + .unwrap(); + vec![button1, button2] + }; + + let action_axes_common: Vec<Action<f32>> = { + let axis1: Action<f32> = action_set + .create_action( + &format!("{}_touchpad_x", hand), + &format!("{}_touchpad_x", hand), + &[], + ) + .unwrap(); + let axis2: Action<f32> = action_set + .create_action( + &format!("{}_touchpad_y", hand), + &format!("{}_touchpad_y", hand), + &[], + ) + .unwrap(); + let axis3: Action<f32> = action_set + .create_action( + &format!("{}_thumbstick_x", hand), + &format!("{}_thumbstick_x", hand), + &[], + ) + .unwrap(); + let axis4: Action<f32> = action_set + .create_action( + &format!("{}_thumbstick_y", hand), + &format!("{}_thumbstick_y", hand), + &[], + ) + .unwrap(); + vec![axis1, axis2, axis3, axis4] + }; + + let use_alternate_input_source = supported_interaction_profiles + .contains(&ext_string!(FB_HAND_TRACKING_AIM_EXTENSION_NAME)); + + Self { + id, + action_aim_pose, + action_aim_space, + action_grip_pose, + action_grip_space, + action_click, + action_squeeze, + handedness, + click_state: ClickState::Done, + squeeze_state: ClickState::Done, + menu_gesture_sustain: 0, + hand_tracker, + action_buttons_common, + action_axes_common, + action_buttons_left, + action_buttons_right, + use_alternate_input_source, + } + } + + pub fn setup_inputs<G: Graphics>( + instance: &Instance, + session: &Session<G>, + needs_hands: bool, + supported_interaction_profiles: Vec<&'static str>, + ) -> (ActionSet, Self, Self) { + let action_set = instance.create_action_set("hands", "Hands", 0).unwrap(); + let right_hand = OpenXRInput::new( + InputId(0), + Handedness::Right, + &action_set, + &session, + needs_hands, + supported_interaction_profiles.clone(), + ); + let left_hand = OpenXRInput::new( + InputId(1), + Handedness::Left, + &action_set, + &session, + needs_hands, + supported_interaction_profiles.clone(), + ); + + for profile in INTERACTION_PROFILES { + if let Some(extension_name) = profile.required_extension { + if !supported_interaction_profiles.contains(&ext_string!(extension_name)) { + continue; + } + } + + if profile.path.is_empty() { + continue; + } + + let select = profile.standard_buttons[0]; + let squeeze = Option::from(profile.standard_buttons[1]).filter(|&s| !s.is_empty()); + let mut bindings = right_hand.get_bindings(instance, select, squeeze, &profile); + bindings.extend( + left_hand + .get_bindings(instance, select, squeeze, &profile) + .into_iter(), + ); + + let path_controller = instance + .string_to_path(profile.path) + .expect(format!("Invalid interaction profile path: {}", profile.path).as_str()); + if let Err(_) = + instance.suggest_interaction_profile_bindings(path_controller, &bindings) + { + debug!( + "Interaction profile path not available for this runtime: {:?}", + profile.path + ); + } + } + + session.attach_action_sets(&[&action_set]).unwrap(); + + (action_set, right_hand, left_hand) + } + + fn get_bindings( + &self, + instance: &Instance, + select_name: &str, + squeeze_name: Option<&str>, + interaction_profile: &InteractionProfile, + ) -> Vec<Binding> { + let hand = hand_str(self.handedness); + let path_aim_pose = instance + .string_to_path(&format!("/user/hand/{}/input/aim/pose", hand)) + .expect(&format!( + "Failed to create path for /user/hand/{}/input/aim/pose", + hand + )); + let binding_aim_pose = Binding::new(&self.action_aim_pose, path_aim_pose); + let path_grip_pose = instance + .string_to_path(&format!("/user/hand/{}/input/grip/pose", hand)) + .expect(&format!( + "Failed to create path for /user/hand/{}/input/grip/pose", + hand + )); + let binding_grip_pose = Binding::new(&self.action_grip_pose, path_grip_pose); + let path_click = instance + .string_to_path(&format!("/user/hand/{}/input/{}", hand, select_name)) + .expect(&format!( + "Failed to create path for /user/hand/{}/input/{}", + hand, select_name + )); + let binding_click = Binding::new(&self.action_click, path_click); + + let mut ret = vec![binding_aim_pose, binding_grip_pose, binding_click]; + if let Some(squeeze_name) = squeeze_name { + let path_squeeze = instance + .string_to_path(&format!("/user/hand/{}/input/{}", hand, squeeze_name)) + .expect(&format!( + "Failed to create path for /user/hand/{}/input/{}", + hand, squeeze_name + )); + let binding_squeeze = Binding::new(&self.action_squeeze, path_squeeze); + ret.push(binding_squeeze); + } + + bind_inputs!( + self.action_buttons_common, + interaction_profile.standard_buttons, + hand, + instance, + ret + ); + + if !interaction_profile.left_buttons.is_empty() && hand == "left" { + bind_inputs!( + self.action_buttons_left, + interaction_profile.left_buttons, + hand, + instance, + ret + ); + } else if !interaction_profile.right_buttons.is_empty() && hand == "right" { + bind_inputs!( + self.action_buttons_right, + interaction_profile.right_buttons, + hand, + instance, + ret + ); + } + + bind_inputs!( + self.action_axes_common, + interaction_profile.standard_axes, + hand, + instance, + ret + ); + + ret + } + + pub fn frame<G: Graphics>( + &mut self, + session: &Session<G>, + frame_state: &FrameState, + base_space: &Space, + viewer: &RigidTransform3D<f32, Viewer, Native>, + ) -> Frame { + use euclid::Vector3D; + let mut target_ray_origin = pose_for(&self.action_aim_space, frame_state, base_space); + + let grip_origin = pose_for(&self.action_grip_space, frame_state, base_space); + + let mut menu_selected = false; + // Check if the palm is facing up. This is our "menu" gesture. + if let Some(grip_origin) = grip_origin { + // The X axis of the grip is perpendicular to the palm, however its + // direction is the opposite for each hand + // + // We obtain a unit vector pointing out of the palm + let x_dir = if let Handedness::Left = self.handedness { + 1.0 + } else { + -1.0 + }; + // Rotate it by the grip to obtain the desired vector + let grip_x = grip_origin + .rotation + .transform_vector3d(Vector3D::new(x_dir, 0.0, 0.0)); + let gaze = viewer + .rotation + .transform_vector3d(Vector3D::new(0., 0., 1.)); + + // If the angle is close enough to 0, its cosine will be + // close to 1 + // check if the user's gaze is parallel to the palm + if gaze.dot(grip_x) > 0.95 { + let input_relative = (viewer.translation - grip_origin.translation).normalize(); + // if so, check if the user is actually looking at the palm + if gaze.dot(input_relative) > 0.95 { + self.menu_gesture_sustain += 1; + if self.menu_gesture_sustain > MENU_GESTURE_SUSTAIN_THRESHOLD { + menu_selected = true; + self.menu_gesture_sustain = 0; + } + } else { + self.menu_gesture_sustain = 0 + } + } else { + self.menu_gesture_sustain = 0; + } + } else { + self.menu_gesture_sustain = 0; + } + + let hand = hand_str(self.handedness); + let click = self.action_click.state(session, Path::NULL).unwrap(); + let squeeze = self.action_squeeze.state(session, Path::NULL).unwrap(); + let (button_values, buttons_changed) = { + let mut changed = false; + let mut values = Vec::<f32>::new(); + let mut sync_buttons = |actions: &Vec<Action<f32>>| { + let buttons = actions + .iter() + .map(|action| { + let state = action.state(session, Path::NULL).unwrap(); + changed = changed || state.changed_since_last_sync; + state.current_state + }) + .collect::<Vec<f32>>(); + values.extend_from_slice(&buttons); + }; + sync_buttons(&self.action_buttons_common); + if hand == "left" { + sync_buttons(&self.action_buttons_left); + } else if hand == "right" { + sync_buttons(&self.action_buttons_right); + } + (values, changed) + }; + + let (axis_values, axes_changed) = { + let mut changed = false; + let values = self + .action_axes_common + .iter() + .enumerate() + .map(|(i, action)| { + let state = action.state(session, Path::NULL).unwrap(); + changed = changed || state.changed_since_last_sync; + // Invert input from y axes + state.current_state * if i % 2 == 1 { -1.0 } else { 1.0 } + }) + .collect::<Vec<f32>>(); + (values, changed) + }; + + let input_changed = buttons_changed || axes_changed; + + let (click_is_active, mut click_event) = if !self.use_alternate_input_source { + self.click_state + .update_from_action(&self.action_click, session, menu_selected) + } else { + (true, None) + }; + let (squeeze_is_active, squeeze_event) = + self.squeeze_state + .update_from_action(&self.action_squeeze, session, menu_selected); + + let mut aim_state: Option<HandTrackingAimStateFB> = None; + let hand = self.hand_tracker.as_ref().and_then(|tracker| { + locate_hand( + base_space, + tracker, + frame_state, + self.use_alternate_input_source, + session, + &mut aim_state, + ) + }); + + let mut pressed = click_is_active && click.current_state; + let squeezed = squeeze_is_active && squeeze.current_state; + + if let Some(state) = aim_state { + target_ray_origin.replace(super::transform(&state.aim_pose)); + let index_pinching = state + .status + .intersects(HandTrackingAimFlagsFB::INDEX_PINCHING); + click_event = self + .click_state + .update_from_value(index_pinching, true, menu_selected); + pressed = index_pinching; + } + + let input_frame = InputFrame { + target_ray_origin, + id: self.id, + pressed, + squeezed, + grip_origin, + hand, + button_values, + axis_values, + input_changed, + }; + + Frame { + frame: input_frame, + select: click_event, + squeeze: squeeze_event, + menu_selected, + } + } + + pub fn input_source(&self) -> InputSource { + let hand_support = if self.hand_tracker.is_some() { + // openxr runtimes must always support all or none joints + Some(Hand::<()>::default().map(|_, _| Some(()))) + } else { + None + }; + InputSource { + handedness: self.handedness, + id: self.id, + target_ray_mode: TargetRayMode::TrackedPointer, + supports_grip: true, + profiles: vec![], + hand_support, + } + } +} + +fn pose_for( + action_space: &Space, + frame_state: &FrameState, + base_space: &Space, +) -> Option<RigidTransform3D<f32, Input, Native>> { + let location = action_space + .locate(base_space, frame_state.predicted_display_time) + .unwrap(); + let pose_valid = location + .location_flags + .intersects(SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::ORIENTATION_VALID); + if pose_valid { + Some(super::transform(&location.pose)) + } else { + None + } +} + +fn locate_hand<G: Graphics>( + base_space: &Space, + tracker: &HandTracker, + frame_state: &FrameState, + use_alternate_input_source: bool, + session: &Session<G>, + aim_state: &mut Option<HandTrackingAimStateFB>, +) -> Option<Box<Hand<JointFrame>>> { + let mut state = HandTrackingAimStateFB::out(std::ptr::null_mut()); + let locations = { + if !use_alternate_input_source { + base_space.locate_hand_joints(tracker, frame_state.predicted_display_time) + } else { + let locate_info = HandJointsLocateInfoEXT { + ty: HandJointsLocateInfoEXT::TYPE, + next: std::ptr::null(), + base_space: base_space.as_raw(), + time: frame_state.predicted_display_time, + }; + + let mut locations = MaybeUninit::<[HandJointLocation; HAND_JOINT_COUNT]>::uninit(); + let mut location_info = HandJointLocationsEXT { + ty: HandJointLocationsEXT::TYPE, + next: &mut state as *mut _ as *mut c_void, + is_active: false.into(), + joint_count: HAND_JOINT_COUNT as u32, + joint_locations: locations.as_mut_ptr() as _, + }; + + // Check if hand tracking is supported by the session instance + let raw_hand_tracker = session.instance().exts().ext_hand_tracking.as_ref()?; + + unsafe { + Ok( + match (raw_hand_tracker.locate_hand_joints)( + tracker.as_raw(), + &locate_info, + &mut location_info, + ) { + openxr::sys::Result::SUCCESS if location_info.is_active.into() => { + aim_state.replace(state.assume_init()); + Some(locations.assume_init()) + } + _ => None, + }, + ) + } + } + }; + let locations = if let Ok(Some(ref locations)) = locations { + Hand { + wrist: Some(&locations[HandJoint::WRIST]), + thumb_metacarpal: Some(&locations[HandJoint::THUMB_METACARPAL]), + thumb_phalanx_proximal: Some(&locations[HandJoint::THUMB_PROXIMAL]), + thumb_phalanx_distal: Some(&locations[HandJoint::THUMB_DISTAL]), + thumb_phalanx_tip: Some(&locations[HandJoint::THUMB_TIP]), + index: Finger { + metacarpal: Some(&locations[HandJoint::INDEX_METACARPAL]), + phalanx_proximal: Some(&locations[HandJoint::INDEX_PROXIMAL]), + phalanx_intermediate: Some(&locations[HandJoint::INDEX_INTERMEDIATE]), + phalanx_distal: Some(&locations[HandJoint::INDEX_DISTAL]), + phalanx_tip: Some(&locations[HandJoint::INDEX_TIP]), + }, + middle: Finger { + metacarpal: Some(&locations[HandJoint::MIDDLE_METACARPAL]), + phalanx_proximal: Some(&locations[HandJoint::MIDDLE_PROXIMAL]), + phalanx_intermediate: Some(&locations[HandJoint::MIDDLE_INTERMEDIATE]), + phalanx_distal: Some(&locations[HandJoint::MIDDLE_DISTAL]), + phalanx_tip: Some(&locations[HandJoint::MIDDLE_TIP]), + }, + ring: Finger { + metacarpal: Some(&locations[HandJoint::RING_METACARPAL]), + phalanx_proximal: Some(&locations[HandJoint::RING_PROXIMAL]), + phalanx_intermediate: Some(&locations[HandJoint::RING_INTERMEDIATE]), + phalanx_distal: Some(&locations[HandJoint::RING_DISTAL]), + phalanx_tip: Some(&locations[HandJoint::RING_TIP]), + }, + little: Finger { + metacarpal: Some(&locations[HandJoint::LITTLE_METACARPAL]), + phalanx_proximal: Some(&locations[HandJoint::LITTLE_PROXIMAL]), + phalanx_intermediate: Some(&locations[HandJoint::LITTLE_INTERMEDIATE]), + phalanx_distal: Some(&locations[HandJoint::LITTLE_DISTAL]), + phalanx_tip: Some(&locations[HandJoint::LITTLE_TIP]), + }, + } + } else { + return None; + }; + + Some(Box::new(locations.map(|loc, _| { + loc.and_then(|location| { + let pose_valid = location.location_flags.intersects( + SpaceLocationFlags::POSITION_VALID | SpaceLocationFlags::ORIENTATION_VALID, + ); + if pose_valid { + Some(JointFrame { + pose: super::transform(&location.pose), + radius: location.radius, + }) + } else { + None + } + }) + }))) +} |