From 029b1864e573bbf0ada540322da60e5bf22ee875 Mon Sep 17 00:00:00 2001 From: Ethan Porcaro Date: Thu, 4 Jun 2026 09:24:03 -0600 Subject: [PATCH 1/2] Actions: Added button and trigger recording support. --- tracking_toolkit/utils.py | 30 ++- tracking_toolkit/xr_core/core.py | 289 ++++++++++++++++++++++++++- tracking_toolkit/xr_core/tracking.py | 85 +++++++- 3 files changed, 384 insertions(+), 20 deletions(-) diff --git a/tracking_toolkit/utils.py b/tracking_toolkit/utils.py index c7a5be7..40c14af 100644 --- a/tracking_toolkit/utils.py +++ b/tracking_toolkit/utils.py @@ -368,13 +368,21 @@ def convert_bones_to_empties(): fcurves = anim_utils.action_get_channelbag_for_slot( empty_action, empty_action.slots[0] ).fcurves - for fcurve in fcurves: - # Only get the fcurve for the current tracker's bone. - if not fcurve.data_path.startswith(f'pose.bones["{nickname}"].'): + for fcurve in fcurves[:]: + # Remove copied fcurves that belong to other bones. + if not fcurve.data_path.startswith(f'pose.bones["{nickname}"]'): fcurves.remove(fcurve) continue - new_path = re.sub(r".+\.([^.]+)$", r"\1", fcurve.data_path) + new_path = re.sub(r".+]\.?(.+)$", r"\1", fcurve.data_path) + + # Ensure custom prop exists. + # It is a custom prop if surrounded by brackets and quotes. + # Strip these brackets and quotes for the custom prop name. + if new_path.startswith('["'): + prop_name = re.sub(r"\[\"(.+)\"]", r"\1", new_path) + empty[prop_name] = float(0) + fcurve.data_path = new_path # If the armature had an active (non-strip) action, set it as active. @@ -503,7 +511,19 @@ def convert_empties_to_bones(): ).fcurves for empty_fcurve in empty_fcurves: - new_path = f'pose.bones["{nickname}"].{empty_fcurve.data_path}' + data_path = empty_fcurve.data_path + + # If surrounded by quotes and brackets, this is a custom property fcurve. + # Ensure the custom property exists. + if empty_fcurve.data_path.startswith('["'): + new_path = f'pose.bones["{nickname}"]{data_path}' + prop_name = re.sub(r"\[\"(.+)\"]", r"\1", data_path) + bone[prop_name] = float(0) + + # Otherwise, standard channel (loc, rot, etc.) + # Use a dot instead of quotes and brackets. + else: + new_path = f'pose.bones["{nickname}"].{data_path}' # Remove existing fcurve if present. arm_fcurve = arm_fcurves.find(new_path, index=empty_fcurve.array_index) diff --git a/tracking_toolkit/xr_core/core.py b/tracking_toolkit/xr_core/core.py index a257ad4..4f72ee0 100644 --- a/tracking_toolkit/xr_core/core.py +++ b/tracking_toolkit/xr_core/core.py @@ -1,6 +1,7 @@ import ctypes import ctypes.wintypes import time +from ctypes import pointer import bpy import gpu @@ -9,10 +10,24 @@ import xr from xr.utils.gl import ContextObject from xr.utils.gl.glfw_util import GLFWOffscreenContextProvider +from dataclasses import dataclass from .actions import default_action_data, vive_tracker_action_data +@dataclass +class PoseData: + pose: mathutils.Matrix + trigger: float + grip: float + thumbstick_x: float + thumbstick_y: float + button_a: float + button_b: float + button_x: float + button_y: float + + def _pose_to_mat(pose): loc = mathutils.Vector(pose.position.as_numpy()) rot = mathutils.Quaternion( @@ -27,6 +42,15 @@ def _pose_to_mat(pose): use_compatibility_mode = False context: ContextObject | None = None spaces = {} +action_data = default_action_data.copy() +action_trigger = None +action_squeeze = None +action_thumbstick_x = None +action_thumbstick_y = None +action_a_click = None +action_b_click = None +action_x_click = None +action_y_click = None def _headless_enter(self): @@ -136,7 +160,9 @@ def start_xr(): bpy.context.window_manager.XRState.runtime = runtime_name print(f"Using {runtime_name} as OpenXR runtime.") - # Setup actions + # Setup actions. + + global action_data action_data = default_action_data.copy() if use_vive_trackers: action_data.extend(vive_tracker_action_data) @@ -155,8 +181,91 @@ def start_xr(): ), ) - # Controller suggested bindings - suggested_bindings = [ + global action_trigger, action_squeeze + global action_thumbstick_x, action_thumbstick_y, action_a_click, action_b_click, action_x_click, action_y_click + action_trigger = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.FLOAT_INPUT, + action_name="tracker_trigger", + localized_action_name="Tracker Trigger", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_squeeze = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.FLOAT_INPUT, + action_name="tracker_squeeze", + localized_action_name="Tracker Squeeze", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_thumbstick_x = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.FLOAT_INPUT, + action_name="tracker_thumbstick_x", + localized_action_name="Tracker Thumbstick X", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_thumbstick_y = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.FLOAT_INPUT, + action_name="tracker_thumbstick_y", + localized_action_name="Tracker Thumbstick Y", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_a_click = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.BOOLEAN_INPUT, + action_name="tracker_a_click", + localized_action_name="Tracker A Click", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_b_click = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.BOOLEAN_INPUT, + action_name="tracker_b_click", + localized_action_name="Tracker B Click", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_x_click = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.BOOLEAN_INPUT, + action_name="tracker_x_click", + localized_action_name="Tracker X Click", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + action_y_click = xr.create_action( + action_set=context.default_action_set, + create_info=xr.ActionCreateInfo( + action_type=xr.ActionType.BOOLEAN_INPUT, + action_name="tracker_y_click", + localized_action_name="Tracker Y Click", + count_subaction_paths=len(paths), + subaction_paths=paths, + ), + ) + + # Base controller suggested bindings. + simple_bindings = [ xr.ActionSuggestedBinding( action=action, binding=xr.string_to_path( @@ -173,11 +282,125 @@ def start_xr(): interaction_profile=xr.string_to_path( context.instance, "/interaction_profiles/khr/simple_controller" ), - count_suggested_bindings=len(suggested_bindings), - suggested_bindings=suggested_bindings, + count_suggested_bindings=len(simple_bindings), + suggested_bindings=simple_bindings, ), ) + # Advanced controller bindings (trigger, squeeze, thumbsticks, buttons). + oculus_bindings = list(simple_bindings) + index_bindings = list(simple_bindings) + for data in default_action_data: + for bindings in [oculus_bindings, index_bindings]: + bindings.extend( + [ + xr.ActionSuggestedBinding( + action=action_trigger, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/trigger/value" + ), + ), + xr.ActionSuggestedBinding( + action=action_squeeze, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/squeeze/value" + ), + ), + xr.ActionSuggestedBinding( + action=action_thumbstick_x, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/thumbstick/x" + ), + ), + xr.ActionSuggestedBinding( + action=action_thumbstick_y, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/thumbstick/y" + ), + ), + ] + ) + + if data.name == "left_hand": + oculus_bindings.extend( + [ + xr.ActionSuggestedBinding( + action=action_x_click, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/x/click" + ), + ), + xr.ActionSuggestedBinding( + action=action_y_click, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/y/click" + ), + ), + ] + ) + index_bindings.extend( + [ + xr.ActionSuggestedBinding( + action=action_a_click, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/a/click" + ), + ), + xr.ActionSuggestedBinding( + action=action_b_click, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/b/click" + ), + ), + ] + ) + elif data.name == "right_hand": + for bindings in [oculus_bindings, index_bindings]: + bindings.extend( + [ + xr.ActionSuggestedBinding( + action=action_a_click, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/a/click" + ), + ), + xr.ActionSuggestedBinding( + action=action_b_click, + binding=xr.string_to_path( + context.instance, data.action_path + "/input/b/click" + ), + ), + ] + ) + + try: + xr.suggest_interaction_profile_bindings( + instance=context.instance, + suggested_bindings=xr.InteractionProfileSuggestedBinding( + interaction_profile=xr.string_to_path( + context.instance, "/interaction_profiles/oculus/touch_controller" + ), + count_suggested_bindings=len(oculus_bindings), + suggested_bindings=oculus_bindings, + ), + ) + except xr.exception.PathUnsupportedError: + pass + + try: + xr.suggest_interaction_profile_bindings( + instance=context.instance, + suggested_bindings=xr.InteractionProfileSuggestedBinding( + interaction_profile=xr.string_to_path( + context.instance, "/interaction_profiles/valve/index_controller" + ), + count_suggested_bindings=len(index_bindings), + suggested_bindings=index_bindings, + ), + ) + except xr.exception.PathUnsupportedError: + pass + # Vive tracker suggested bindings if use_vive_trackers: suggested_bindings = [ @@ -218,7 +441,7 @@ def start_xr(): session=context.session, attach_info=xr.SessionActionSetsAttachInfo( count_action_sets=len(context.action_sets), - action_sets=(xr.ActionSet * len(context.action_sets))(*context.action_sets), + action_sets=pointer(context.default_action_set), ), ) @@ -337,7 +560,47 @@ def tick_xr(): ) if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT: - poses[space_name] = _pose_to_mat(space_location.pose) + # Get float state for trigger and squeeze + subaction_path = xr.string_to_path( + context.instance, + [d.action_path for d in action_data if d.name == space_name][0], + ) + + def get_float(action) -> float: + try: + state = xr.get_action_state_float( + session=context.session, + get_info=xr.ActionStateGetInfo( + action=action, subaction_path=subaction_path + ), + ) + return state.current_state if state.is_active else 0.0 + except xr.XrException: + return 0.0 + + def get_bool(action) -> float: + try: + state = xr.get_action_state_boolean( + session=context.session, + get_info=xr.ActionStateGetInfo( + action=action, subaction_path=subaction_path + ), + ) + return float(state.current_state) if state.is_active else 0.0 + except xr.XrException: + return 0.0 + + poses[space_name] = PoseData( + pose=_pose_to_mat(space_location.pose), + trigger=get_float(action_trigger), + grip=get_float(action_squeeze), + thumbstick_x=get_float(action_thumbstick_x), + thumbstick_y=get_float(action_thumbstick_y), + button_a=get_bool(action_a_click), + button_b=get_bool(action_b_click), + button_x=get_bool(action_x_click), + button_y=get_bool(action_y_click), + ) # Get HMD pose view_state, views = xr.locate_views( @@ -348,7 +611,17 @@ def tick_xr(): space=context.space, ), ) - poses["head"] = _pose_to_mat(views[xr.utils.Eye.LEFT.value].pose) + poses["head"] = PoseData( + pose=_pose_to_mat(views[xr.utils.Eye.LEFT.value].pose), + trigger=0.0, + grip=0.0, + thumbstick_x=0.0, + thumbstick_y=0.0, + button_a=0.0, + button_b=0.0, + button_x=0.0, + button_y=0.0, + ) if len(poses) == 0: return None diff --git a/tracking_toolkit/xr_core/tracking.py b/tracking_toolkit/xr_core/tracking.py index 81eccbf..006ab44 100644 --- a/tracking_toolkit/xr_core/tracking.py +++ b/tracking_toolkit/xr_core/tracking.py @@ -5,7 +5,7 @@ from bpy_extras import anim_utils from .actions import vive_role_strings -from .core import start_xr, tick_xr, stop_xr +from .core import start_xr, tick_xr, stop_xr, PoseData from ..preferences import get_preferences from ..utils import get_context, get_state @@ -79,12 +79,12 @@ def _clear_buffer(): data_buffer.clear() -def _get_buffer() -> list[tuple[datetime.datetime, dict[str, mathutils.Matrix]]]: +def _get_buffer() -> list[tuple[datetime.datetime, dict[str, PoseData]]]: global data_buffer return data_buffer.copy() -def _get_latest_poses() -> dict[str, mathutils.Matrix] | None: +def _get_latest_poses() -> dict[str, PoseData] | None: global data_buffer if len(data_buffer) == 0: return None @@ -120,7 +120,15 @@ def _apply_poses(): if not bone.get("ref_type") == "tracker": continue - bone.matrix = pose + bone.matrix = pose.pose + bone["trigger"] = pose.trigger + bone["grip"] = pose.grip + bone["thumbstick_x"] = pose.thumbstick_x + bone["thumbstick_y"] = pose.thumbstick_y + bone["button_a"] = pose.button_a + bone["button_b"] = pose.button_b + bone["button_x"] = pose.button_x + bone["button_y"] = pose.button_y # Apply empty transforms. else: @@ -131,7 +139,15 @@ def _apply_poses(): if not obj.get("ref_type") == "tracker": continue - obj.matrix_world = pose + obj.matrix_world = pose.pose + obj["trigger"] = pose.trigger + obj["grip"] = pose.grip + obj["thumbstick_x"] = pose.thumbstick_x + obj["thumbstick_y"] = pose.thumbstick_y + obj["button_a"] = pose.button_a + obj["button_b"] = pose.button_b + obj["button_x"] = pose.button_x + obj["button_y"] = pose.button_y def _pose_vis_timer(): @@ -252,6 +268,14 @@ def _insert_action(): "locs": [], "rots": [], "scales": [], + "triggers": [], + "grips": [], + "thumbstick_xs": [], + "thumbstick_ys": [], + "button_as": [], + "button_bs": [], + "button_xs": [], + "button_ys": [], } # Lerp pose. @@ -260,13 +284,36 @@ def _insert_action(): continue prev_pose = prev_sample[name] - loc0, rot0, sca0 = prev_pose.decompose() - loc1, rot1, sca1 = next_pose.decompose() + loc0, rot0, sca0 = prev_pose.pose.decompose() + loc1, rot1, sca1 = next_pose.pose.decompose() loc_final = loc0.lerp(loc1, factor) rot_final = rot0.slerp(rot1, factor) # Slerp for rotation. sca_final = sca0.lerp(sca1, factor) + trigger0 = prev_pose.trigger + trigger1 = next_pose.trigger + trigger_final = trigger0 + (trigger1 - trigger0) * factor + + grip0 = prev_pose.grip + grip1 = next_pose.grip + grip_final = grip0 + (grip1 - grip0) * factor + + ts_x0 = prev_pose.thumbstick_x + ts_x1 = next_pose.thumbstick_x + ts_x_final = ts_x0 + (ts_x1 - ts_x0) * factor + + ts_y0 = prev_pose.thumbstick_y + ts_y1 = next_pose.thumbstick_y + ts_y_final = ts_y0 + (ts_y1 - ts_y0) * factor + + # Binarize buttons. + btn_pose = next_pose if factor > 0.5 else prev_pose + a_final = btn_pose.button_a + b_final = btn_pose.button_b + x_final = btn_pose.button_x + y_final = btn_pose.button_y + lerp_pose = mathutils.Matrix.LocRotScale(loc_final, rot_final, sca_final) # Decompose the matrix and append data. @@ -277,6 +324,14 @@ def _insert_action(): data["locs"].extend(loc) data["rots"].extend(rot) data["scales"].extend(scale) + data["triggers"].append(trigger_final) + data["grips"].append(grip_final) + data["thumbstick_xs"].append(ts_x_final) + data["thumbstick_ys"].append(ts_y_final) + data["button_as"].append(a_final) + data["button_bs"].append(b_final) + data["button_xs"].append(x_final) + data["button_ys"].append(y_final) # Increment. current_time += 1 / record_fps @@ -341,12 +396,28 @@ def _insert_action(): (f"{data_path_prefix}.location", 3, data["locs"]), (f"{data_path_prefix}.rotation_quaternion", 4, data["rots"]), (f"{data_path_prefix}.scale", 3, data["scales"]), + (f'{data_path_prefix}["trigger"]', 1, data["triggers"]), + (f'{data_path_prefix}["grip"]', 1, data["grips"]), + (f'{data_path_prefix}["thumbstick_x"]', 1, data["thumbstick_xs"]), + (f'{data_path_prefix}["thumbstick_y"]', 1, data["thumbstick_ys"]), + (f'{data_path_prefix}["button_a"]', 1, data["button_as"]), + (f'{data_path_prefix}["button_b"]', 1, data["button_bs"]), + (f'{data_path_prefix}["button_x"]', 1, data["button_xs"]), + (f'{data_path_prefix}["button_y"]', 1, data["button_ys"]), ] else: fcurve_props = [ ("location", 3, data["locs"]), ("rotation_quaternion", 4, data["rots"]), ("scale", 3, data["scales"]), + ('["trigger"]', 1, data["triggers"]), + ('["grip"]', 1, data["grips"]), + ('["thumbstick_x"]', 1, data["thumbstick_xs"]), + ('["thumbstick_y"]', 1, data["thumbstick_ys"]), + ('["button_a"]', 1, data["button_as"]), + ('["button_b"]', 1, data["button_bs"]), + ('["button_x"]', 1, data["button_xs"]), + ('["button_y"]', 1, data["button_ys"]), ] # Efficiently insert animation data by directly inserting it into the fcurves. From 14fe200234772422268106c0af579cefdff9b8b0 Mon Sep 17 00:00:00 2001 From: Ethan Porcaro Date: Thu, 9 Jul 2026 20:41:26 -0600 Subject: [PATCH 2/2] Inputs: Controller buttons now trigger certain events instead of just recording data. --- __init__.py | 2 + tracking_toolkit/preferences.py | 88 +++++++-- tracking_toolkit/xr_core/actions.py | 31 --- tracking_toolkit/xr_core/core.py | 285 ++++++++++++++------------- tracking_toolkit/xr_core/tracking.py | 158 ++++++++------- 5 files changed, 306 insertions(+), 258 deletions(-) diff --git a/__init__.py b/__init__.py index 9ccc767..fbb1571 100644 --- a/__init__.py +++ b/__init__.py @@ -60,6 +60,7 @@ def register(): # Prefs bpy.utils.register_class(preferences.PreferenceNaming) + bpy.utils.register_class(preferences.PreferenceInputMapping) bpy.utils.register_class(preferences.ResetNicknamesOperator) bpy.utils.register_class(preferences.Preferences) preferences.initialize_preferences() @@ -107,6 +108,7 @@ def unregister(): # Prefs bpy.utils.unregister_class(preferences.Preferences) bpy.utils.unregister_class(preferences.ResetNicknamesOperator) + bpy.utils.unregister_class(preferences.PreferenceInputMapping) bpy.utils.unregister_class(preferences.PreferenceNaming) # Props diff --git a/tracking_toolkit/preferences.py b/tracking_toolkit/preferences.py index 10caa9d..71b678e 100644 --- a/tracking_toolkit/preferences.py +++ b/tracking_toolkit/preferences.py @@ -89,37 +89,91 @@ class PreferenceNaming(bpy.types.PropertyGroup): ) +class PreferenceInputMapping(bpy.types.PropertyGroup): + # Reusable types. + INPUT_TYPE_PROPERTY = bpy.props.EnumProperty( + name="Input Type", + items=[ + ("Trigger", "Trigger", "Controller Trigger"), + ("Grip", "Grip", "Controller Grip"), + ("A", "A", "A Button"), + ("B", "B", "B Button"), + ("X", "X", "X Button"), + ("Y", "Y", "Y Button"), + ], + ) + INPUT_ROLE_PROPERTY = bpy.props.EnumProperty( + name="Input Tracker Role", + items=[ + ("None", "None", "None"), + *[(n, n, n) for i, n in enumerate(all_role_strings)], + ], + ) + + # Actual properties. + toggle_capture_role: INPUT_ROLE_PROPERTY + toggle_capture_input: INPUT_TYPE_PROPERTY + frame_forward_role: INPUT_ROLE_PROPERTY + frame_forward_input: INPUT_TYPE_PROPERTY + frame_backward_role: INPUT_ROLE_PROPERTY + frame_backward_input: INPUT_TYPE_PROPERTY + + class Preferences(bpy.types.AddonPreferences): bl_idname = base_package record_at_scene_fps: bpy.props.BoolProperty(default=True) record_custom_fps: bpy.props.IntProperty(default=24, min=1, max=120, soft_max=90) + input_mapping: bpy.props.PointerProperty(type=PreferenceInputMapping) + naming: bpy.props.CollectionProperty( name="Default Tracker Nicknames", type=PreferenceNaming ) - def draw(self, _): - layout = self.layout + def _draw_recording_options(self): + rec_box = self.layout.box() + rec_box.label(text="Recording Options", icon="TIME") - layout.label(text="Recording Options") - - layout.prop(self, "record_at_scene_fps", text="Record at Scene FPS") + rec_box.prop(self, "record_at_scene_fps", text="Record at Scene FPS") if not self.record_at_scene_fps: - layout.prop(self, "record_custom_fps", text="Custom FPS") - layout.label(text="Warning: Using custom FPS. Subframes may be created.") - layout.label(text="High scene or custom FPS can cause performance issues.") - - layout.separator_spacer() - - # Tracker nickname options. - - layout.label(text="Tracker Nicknames") - layout.label( + rec_box.prop(self, "record_custom_fps", text="Custom FPS") + rec_box.label(text="Warning: Using custom FPS. Subframes may be created.") + rec_box.label(text="High scene or custom FPS can cause performance issues.") + + def _draw_input_options(self): + ipt_box = self.layout.box() + ipt_box.label(text="Input Mapping", icon="MOUSE_LMB") + + def _draw_input_map(text: str, action_name: str): + """ + Utility to draw a tracker role/input mapping. + """ + row = ipt_box.row() + if getattr(self.input_mapping, f"{action_name}_role", None) == "None": + row.prop(self.input_mapping, f"{action_name}_role", text=text) + row.label(text="") + else: + row.prop(self.input_mapping, f"{action_name}_role", text=text) + row.prop(self.input_mapping, f"{action_name}_input", text="") + + _draw_input_map("Start/Stop Capture", "toggle_capture") + _draw_input_map("Frame Forward", "frame_forward") + _draw_input_map("Frame Backward", "frame_backward") + + def _draw_nickname_options(self): + nn_box = self.layout.box() + nn_box.label(text="Tracker Nicknames", icon="TEXT") + nn_box.label( text="These apply going forward, and will not replace the current nicknames in your scene." ) for n in self.naming: - layout.prop(n, "nickname", text=n.role_string) + nn_box.prop(n, "nickname", text=n.role_string) + + nn_box.operator(ResetNicknamesOperator.bl_idname, text="Reset Nicknames") - layout.operator(ResetNicknamesOperator.bl_idname, text="Reset Nicknames") + def draw(self, _): + self._draw_recording_options() + self._draw_input_options() + self._draw_nickname_options() diff --git a/tracking_toolkit/xr_core/actions.py b/tracking_toolkit/xr_core/actions.py index d434362..20a22a0 100644 --- a/tracking_toolkit/xr_core/actions.py +++ b/tracking_toolkit/xr_core/actions.py @@ -1,27 +1,4 @@ import re -from dataclasses import dataclass - - -@dataclass -class ActionData: - name: str - action_path: str - subaction_path: str - - -# Default actions. -default_action_data = [ - ActionData( - name="left_hand", - action_path="/user/hand/left", - subaction_path="/input/grip/pose", - ), - ActionData( - name="right_hand", - action_path="/user/hand/right", - subaction_path="/input/grip/pose", - ), -] # Vive tracker actions. vive_role_strings = [ @@ -43,14 +20,6 @@ class ActionData: "camera", "keyboard", ] -vive_tracker_action_data = [ - ActionData( - name=role, - action_path=f"/user/vive_tracker_htcx/role/{role}", - subaction_path="/input/grip/pose", - ) - for role in vive_role_strings -] all_role_strings = ["head", "left_hand", "right_hand", *vive_role_strings] diff --git a/tracking_toolkit/xr_core/core.py b/tracking_toolkit/xr_core/core.py index 4f72ee0..2cc5b8a 100644 --- a/tracking_toolkit/xr_core/core.py +++ b/tracking_toolkit/xr_core/core.py @@ -12,7 +12,7 @@ from xr.utils.gl.glfw_util import GLFWOffscreenContextProvider from dataclasses import dataclass -from .actions import default_action_data, vive_tracker_action_data +from .actions import vive_role_strings @dataclass @@ -42,7 +42,6 @@ def _pose_to_mat(pose): use_compatibility_mode = False context: ContextObject | None = None spaces = {} -action_data = default_action_data.copy() action_trigger = None action_squeeze = None action_thumbstick_x = None @@ -162,35 +161,39 @@ def start_xr(): # Setup actions. - global action_data - action_data = default_action_data.copy() - if use_vive_trackers: - action_data.extend(vive_tracker_action_data) + global action_trigger, action_squeeze + global action_thumbstick_x, action_thumbstick_y, action_a_click, action_b_click, action_x_click, action_y_click - paths = [ - xr.string_to_path(context.instance, data.action_path) for data in action_data + controller_path_strings = ["/user/hand/left", "/user/hand/right"] + controller_paths = [ + xr.string_to_path(context.instance, name) for name in controller_path_strings + ] + tracker_path_strings = [ + f"/user/vive_tracker_htcx/role/{role}" for role in vive_role_strings + ] + tracker_paths = [ + xr.string_to_path(context.instance, name) for name in tracker_path_strings ] - action = xr.create_action( + combined_paths = [*controller_paths, *tracker_paths] + + action_pose = xr.create_action( action_set=context.default_action_set, create_info=xr.ActionCreateInfo( action_type=xr.ActionType.POSE_INPUT, action_name="tracker_pose", localized_action_name="Tracker Pose", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(combined_paths), + subaction_paths=combined_paths, ), ) - - global action_trigger, action_squeeze - global action_thumbstick_x, action_thumbstick_y, action_a_click, action_b_click, action_x_click, action_y_click action_trigger = xr.create_action( action_set=context.default_action_set, create_info=xr.ActionCreateInfo( action_type=xr.ActionType.FLOAT_INPUT, action_name="tracker_trigger", localized_action_name="Tracker Trigger", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(combined_paths), + subaction_paths=combined_paths, ), ) action_squeeze = xr.create_action( @@ -199,8 +202,8 @@ def start_xr(): action_type=xr.ActionType.FLOAT_INPUT, action_name="tracker_squeeze", localized_action_name="Tracker Squeeze", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(combined_paths), + subaction_paths=combined_paths, ), ) action_thumbstick_x = xr.create_action( @@ -209,8 +212,8 @@ def start_xr(): action_type=xr.ActionType.FLOAT_INPUT, action_name="tracker_thumbstick_x", localized_action_name="Tracker Thumbstick X", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(controller_paths), + subaction_paths=controller_paths, ), ) action_thumbstick_y = xr.create_action( @@ -219,8 +222,8 @@ def start_xr(): action_type=xr.ActionType.FLOAT_INPUT, action_name="tracker_thumbstick_y", localized_action_name="Tracker Thumbstick Y", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(controller_paths), + subaction_paths=controller_paths, ), ) action_a_click = xr.create_action( @@ -229,8 +232,8 @@ def start_xr(): action_type=xr.ActionType.BOOLEAN_INPUT, action_name="tracker_a_click", localized_action_name="Tracker A Click", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(controller_paths), + subaction_paths=controller_paths, ), ) action_b_click = xr.create_action( @@ -239,8 +242,8 @@ def start_xr(): action_type=xr.ActionType.BOOLEAN_INPUT, action_name="tracker_b_click", localized_action_name="Tracker B Click", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(controller_paths), + subaction_paths=controller_paths, ), ) action_x_click = xr.create_action( @@ -249,8 +252,8 @@ def start_xr(): action_type=xr.ActionType.BOOLEAN_INPUT, action_name="tracker_x_click", localized_action_name="Tracker X Click", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(controller_paths), + subaction_paths=controller_paths, ), ) action_y_click = xr.create_action( @@ -259,160 +262,147 @@ def start_xr(): action_type=xr.ActionType.BOOLEAN_INPUT, action_name="tracker_y_click", localized_action_name="Tracker Y Click", - count_subaction_paths=len(paths), - subaction_paths=paths, + count_subaction_paths=len(controller_paths), + subaction_paths=controller_paths, ), ) # Base controller suggested bindings. - simple_bindings = [ + controller_bindings = [ xr.ActionSuggestedBinding( - action=action, + action=action_pose, binding=xr.string_to_path( instance=context.instance, - path_string=data.action_path + data.subaction_path, + path_string=f"{action_path}/input/grip/pose", ), ) - for data in default_action_data + for action_path in controller_path_strings ] - xr.suggest_interaction_profile_bindings( instance=context.instance, suggested_bindings=xr.InteractionProfileSuggestedBinding( interaction_profile=xr.string_to_path( context.instance, "/interaction_profiles/khr/simple_controller" ), - count_suggested_bindings=len(simple_bindings), - suggested_bindings=simple_bindings, + count_suggested_bindings=len(controller_bindings), + suggested_bindings=controller_bindings, ), ) # Advanced controller bindings (trigger, squeeze, thumbsticks, buttons). - oculus_bindings = list(simple_bindings) - index_bindings = list(simple_bindings) - for data in default_action_data: - for bindings in [oculus_bindings, index_bindings]: - bindings.extend( - [ - xr.ActionSuggestedBinding( - action=action_trigger, - binding=xr.string_to_path( - context.instance, data.action_path + "/input/trigger/value" - ), + advanced_bindings = controller_bindings.copy() + for action_path in controller_path_strings: + advanced_bindings.extend( + [ + xr.ActionSuggestedBinding( + action=action_trigger, + binding=xr.string_to_path( + context.instance, action_path + "/input/trigger/value" ), - xr.ActionSuggestedBinding( - action=action_squeeze, - binding=xr.string_to_path( - context.instance, data.action_path + "/input/squeeze/value" - ), + ), + xr.ActionSuggestedBinding( + action=action_squeeze, + binding=xr.string_to_path( + context.instance, action_path + "/input/squeeze/value" ), - xr.ActionSuggestedBinding( - action=action_thumbstick_x, - binding=xr.string_to_path( - context.instance, data.action_path + "/input/thumbstick/x" - ), + ), + xr.ActionSuggestedBinding( + action=action_thumbstick_x, + binding=xr.string_to_path( + context.instance, action_path + "/input/thumbstick/x" ), - xr.ActionSuggestedBinding( - action=action_thumbstick_y, - binding=xr.string_to_path( - context.instance, data.action_path + "/input/thumbstick/y" - ), + ), + xr.ActionSuggestedBinding( + action=action_thumbstick_y, + binding=xr.string_to_path( + context.instance, action_path + "/input/thumbstick/y" ), - ] - ) + ), + ] + ) - if data.name == "left_hand": - oculus_bindings.extend( + # Left hand has X and Y. + if action_path.endswith("left"): + advanced_bindings.extend( [ xr.ActionSuggestedBinding( action=action_x_click, binding=xr.string_to_path( - context.instance, data.action_path + "/input/x/click" + context.instance, action_path + "/input/x/click" ), ), xr.ActionSuggestedBinding( action=action_y_click, binding=xr.string_to_path( - context.instance, data.action_path + "/input/y/click" + context.instance, action_path + "/input/y/click" ), ), ] ) - index_bindings.extend( + + # Right hand has A and B. + elif action_path.endswith("right"): + advanced_bindings.extend( [ xr.ActionSuggestedBinding( action=action_a_click, binding=xr.string_to_path( - context.instance, data.action_path + "/input/a/click" + context.instance, action_path + "/input/a/click" ), ), xr.ActionSuggestedBinding( action=action_b_click, binding=xr.string_to_path( - context.instance, data.action_path + "/input/b/click" + context.instance, action_path + "/input/b/click" ), ), ] ) - elif data.name == "right_hand": - for bindings in [oculus_bindings, index_bindings]: - bindings.extend( - [ - xr.ActionSuggestedBinding( - action=action_a_click, - binding=xr.string_to_path( - context.instance, data.action_path + "/input/a/click" - ), - ), - xr.ActionSuggestedBinding( - action=action_b_click, - binding=xr.string_to_path( - context.instance, data.action_path + "/input/b/click" - ), - ), - ] - ) - try: - xr.suggest_interaction_profile_bindings( - instance=context.instance, - suggested_bindings=xr.InteractionProfileSuggestedBinding( - interaction_profile=xr.string_to_path( - context.instance, "/interaction_profiles/oculus/touch_controller" + # Try to register the advanced bindings with common brands. + profiles = [ + "/interaction_profiles/oculus/touch_controller", + "/interaction_profiles/valve/index_controller", + ] + for profile in profiles: + try: + xr.suggest_interaction_profile_bindings( + instance=context.instance, + suggested_bindings=xr.InteractionProfileSuggestedBinding( + interaction_profile=xr.string_to_path(context.instance, profile), + count_suggested_bindings=len(advanced_bindings), + suggested_bindings=advanced_bindings, ), - count_suggested_bindings=len(oculus_bindings), - suggested_bindings=oculus_bindings, - ), - ) - except xr.exception.PathUnsupportedError: - pass + ) - try: - xr.suggest_interaction_profile_bindings( - instance=context.instance, - suggested_bindings=xr.InteractionProfileSuggestedBinding( - interaction_profile=xr.string_to_path( - context.instance, "/interaction_profiles/valve/index_controller" - ), - count_suggested_bindings=len(index_bindings), - suggested_bindings=index_bindings, - ), - ) - except xr.exception.PathUnsupportedError: - pass + print(f"Enabled advanced bindings for: {profile}") + except xr.exception.PathUnsupportedError: + pass - # Vive tracker suggested bindings + # Vive tracker suggested bindings. + # See https://registry.khronos.org/OpenXR/specs/1.1/html/xrspec.html#XR_HTCX_vive_tracker_interaction if use_vive_trackers: - suggested_bindings = [ - xr.ActionSuggestedBinding( - action=action, - binding=xr.string_to_path( - instance=context.instance, - path_string=data.action_path + data.subaction_path, - ), + tracker_bindings = [] + + for tracker_role in vive_role_strings: + tracker_bindings.extend( + [ + xr.ActionSuggestedBinding( + action=action_pose, + binding=xr.string_to_path( + instance=context.instance, + path_string=f"/user/vive_tracker_htcx/role/{tracker_role}/input/grip/pose", + ), + ), + xr.ActionSuggestedBinding( + action=action_trigger, + binding=xr.string_to_path( + instance=context.instance, + path_string=f"/user/vive_tracker_htcx/role/{tracker_role}/input/trigger/value", + ), + ), + ] ) - for data in vive_tracker_action_data - ] xr.suggest_interaction_profile_bindings( instance=context.instance, @@ -420,22 +410,39 @@ def start_xr(): interaction_profile=xr.string_to_path( context.instance, "/interaction_profiles/htc/vive_tracker_htcx" ), - count_suggested_bindings=len(suggested_bindings), - suggested_bindings=suggested_bindings, + count_suggested_bindings=len(tracker_bindings), + suggested_bindings=tracker_bindings, ), ) - # Create action spaces + # Create action spaces. global spaces - for data in action_data: - spaces[data.name] = xr.create_action_space( + + # Controllers. + for hand_name in ["left", "right"]: + spaces[f"{hand_name}_hand"] = xr.create_action_space( session=context.session, create_info=xr.ActionSpaceCreateInfo( - action=action, - subaction_path=xr.string_to_path(context.instance, data.action_path), + action=action_pose, + subaction_path=xr.string_to_path( + context.instance, f"/user/hand/{hand_name}" + ), ), ) + # Vive trackers. + if use_vive_trackers: + for role_string in vive_role_strings: + spaces[role_string] = xr.create_action_space( + session=context.session, + create_info=xr.ActionSpaceCreateInfo( + action=action_pose, + subaction_path=xr.string_to_path( + context.instance, f"/user/vive_tracker_htcx/role/{role_string}" + ), + ), + ) + # Attach action sets. xr.attach_session_action_sets( session=context.session, @@ -552,7 +559,7 @@ def tick_xr(): poses = {} for space_name in spaces.keys(): - space = spaces[space_name] + space: xr.Space = spaces[space_name] space_location = xr.locate_space( space=space, base_space=context.space, @@ -560,11 +567,13 @@ def tick_xr(): ) if space_location.location_flags & xr.SPACE_LOCATION_POSITION_VALID_BIT: - # Get float state for trigger and squeeze - subaction_path = xr.string_to_path( - context.instance, - [d.action_path for d in action_data if d.name == space_name][0], - ) + # Get float state for trigger and squeeze. + if space_name in ["left_hand", "right_hand"]: + # Unideal workaround. + path_string = f"/user/hand/{space_name.replace('_hand', '')}" + else: + path_string = f"/user/vive_tracker_htcx/role/{space_name}" + subaction_path = xr.string_to_path(context.instance, path_string) def get_float(action) -> float: try: diff --git a/tracking_toolkit/xr_core/tracking.py b/tracking_toolkit/xr_core/tracking.py index 006ab44..acad0c5 100644 --- a/tracking_toolkit/xr_core/tracking.py +++ b/tracking_toolkit/xr_core/tracking.py @@ -6,12 +6,13 @@ from .actions import vive_role_strings from .core import start_xr, tick_xr, stop_xr, PoseData -from ..preferences import get_preferences +from ..preferences import get_preferences, PreferenceInputMapping from ..utils import get_context, get_state # Shared variables data_buffer = [] should_stop = False +armed_triggers = [] def _update_tracker_list(poses): @@ -121,14 +122,6 @@ def _apply_poses(): continue bone.matrix = pose.pose - bone["trigger"] = pose.trigger - bone["grip"] = pose.grip - bone["thumbstick_x"] = pose.thumbstick_x - bone["thumbstick_y"] = pose.thumbstick_y - bone["button_a"] = pose.button_a - bone["button_b"] = pose.button_b - bone["button_x"] = pose.button_x - bone["button_y"] = pose.button_y # Apply empty transforms. else: @@ -140,14 +133,9 @@ def _apply_poses(): continue obj.matrix_world = pose.pose - obj["trigger"] = pose.trigger - obj["grip"] = pose.grip - obj["thumbstick_x"] = pose.thumbstick_x - obj["thumbstick_y"] = pose.thumbstick_y - obj["button_a"] = pose.button_a - obj["button_b"] = pose.button_b - obj["button_x"] = pose.button_x - obj["button_y"] = pose.button_y + + # Handle actions. + _handle_actions(role_string, pose) def _pose_vis_timer(): @@ -155,6 +143,87 @@ def _pose_vis_timer(): return 1.0 / 60 # 60hz +def _handle_actions(role_string: str, pose: PoseData): + """ + Handle the events triggered by actions. The mapping is stored in the preferences menu. + """ + + # Calculate leaped values. + + # Build data. + data = { + "Trigger": pose.trigger, + "Grip": pose.grip, + "A": pose.button_a, + "B": pose.button_b, + "X": pose.button_x, + "Y": pose.button_y, + "Thumbstick X": pose.thumbstick_x, + "Thumbstick Y": pose.thumbstick_y, + } + + # Check actions. + + xr_state = get_state() + preferences = get_preferences() + map_: PreferenceInputMapping = preferences.input_mapping + + def _check_input(action_name: str) -> bool: + """ + Utility to check if an action trigger is met. + """ + role_prop = getattr(map_, f"{action_name}_role", None) + ipt_prop = getattr(map_, f"{action_name}_input", None) + + if not role_prop or not ipt_prop: + return False + + if role_prop != role_string: + return False + + value = data.get(ipt_prop) + if value is None: + return False + + is_pressed = float(value) >= 0.9 + + # Check if the trigger is armed. + # This means that it was held before. + # If it is no longer held, that triggers the event. + global armed_triggers + + is_armed = action_name in armed_triggers + + # If pressed and unarmed, arm the event. + if is_pressed: + if not is_armed: + print(f"Holding {action_name}...") + armed_triggers.append(action_name) + + # If not pressed, and was armed before, fire event and disarm. + else: + if is_armed: + print(f"Triggered {action_name}!") + armed_triggers.remove(action_name) + return True + + return False + + if _check_input("toggle_capture"): + if xr_state.recording: + stop_recording() + else: + start_recording() + + if _check_input("frame_forward"): + if not xr_state.recording: + bpy.context.scene.frame_current += 1 + + if _check_input("frame_backward"): + if not xr_state.recording: + bpy.context.scene.frame_current -= 1 + + def _create_action(obj: bpy.types.Object, action_name: str): """ Create a new action for an object. @@ -268,14 +337,6 @@ def _insert_action(): "locs": [], "rots": [], "scales": [], - "triggers": [], - "grips": [], - "thumbstick_xs": [], - "thumbstick_ys": [], - "button_as": [], - "button_bs": [], - "button_xs": [], - "button_ys": [], } # Lerp pose. @@ -291,29 +352,6 @@ def _insert_action(): rot_final = rot0.slerp(rot1, factor) # Slerp for rotation. sca_final = sca0.lerp(sca1, factor) - trigger0 = prev_pose.trigger - trigger1 = next_pose.trigger - trigger_final = trigger0 + (trigger1 - trigger0) * factor - - grip0 = prev_pose.grip - grip1 = next_pose.grip - grip_final = grip0 + (grip1 - grip0) * factor - - ts_x0 = prev_pose.thumbstick_x - ts_x1 = next_pose.thumbstick_x - ts_x_final = ts_x0 + (ts_x1 - ts_x0) * factor - - ts_y0 = prev_pose.thumbstick_y - ts_y1 = next_pose.thumbstick_y - ts_y_final = ts_y0 + (ts_y1 - ts_y0) * factor - - # Binarize buttons. - btn_pose = next_pose if factor > 0.5 else prev_pose - a_final = btn_pose.button_a - b_final = btn_pose.button_b - x_final = btn_pose.button_x - y_final = btn_pose.button_y - lerp_pose = mathutils.Matrix.LocRotScale(loc_final, rot_final, sca_final) # Decompose the matrix and append data. @@ -324,14 +362,6 @@ def _insert_action(): data["locs"].extend(loc) data["rots"].extend(rot) data["scales"].extend(scale) - data["triggers"].append(trigger_final) - data["grips"].append(grip_final) - data["thumbstick_xs"].append(ts_x_final) - data["thumbstick_ys"].append(ts_y_final) - data["button_as"].append(a_final) - data["button_bs"].append(b_final) - data["button_xs"].append(x_final) - data["button_ys"].append(y_final) # Increment. current_time += 1 / record_fps @@ -396,28 +426,12 @@ def _insert_action(): (f"{data_path_prefix}.location", 3, data["locs"]), (f"{data_path_prefix}.rotation_quaternion", 4, data["rots"]), (f"{data_path_prefix}.scale", 3, data["scales"]), - (f'{data_path_prefix}["trigger"]', 1, data["triggers"]), - (f'{data_path_prefix}["grip"]', 1, data["grips"]), - (f'{data_path_prefix}["thumbstick_x"]', 1, data["thumbstick_xs"]), - (f'{data_path_prefix}["thumbstick_y"]', 1, data["thumbstick_ys"]), - (f'{data_path_prefix}["button_a"]', 1, data["button_as"]), - (f'{data_path_prefix}["button_b"]', 1, data["button_bs"]), - (f'{data_path_prefix}["button_x"]', 1, data["button_xs"]), - (f'{data_path_prefix}["button_y"]', 1, data["button_ys"]), ] else: fcurve_props = [ ("location", 3, data["locs"]), ("rotation_quaternion", 4, data["rots"]), ("scale", 3, data["scales"]), - ('["trigger"]', 1, data["triggers"]), - ('["grip"]', 1, data["grips"]), - ('["thumbstick_x"]', 1, data["thumbstick_xs"]), - ('["thumbstick_y"]', 1, data["thumbstick_ys"]), - ('["button_a"]', 1, data["button_as"]), - ('["button_b"]', 1, data["button_bs"]), - ('["button_x"]', 1, data["button_xs"]), - ('["button_y"]', 1, data["button_ys"]), ] # Efficiently insert animation data by directly inserting it into the fcurves.