diff --git a/demo/Main.tscn b/demo/Main.tscn index 5dec9c8..48a6048 100644 --- a/demo/Main.tscn +++ b/demo/Main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=29 format=2] +[gd_scene load_steps=30 format=2] [ext_resource path="res://Main.gd" type="Script" id=1] [ext_resource path="res://scenes/Ground.tscn" type="PackedScene" id=2] @@ -24,6 +24,7 @@ [ext_resource path="res://scenes/BallCourt.tscn" type="PackedScene" id=22] [ext_resource path="res://addons/godot-openxr/scenes/controller.gd" type="Script" id=23] [ext_resource path="res://scenes/AimPointer.tscn" type="PackedScene" id=24] +[ext_resource path="res://assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.obj" type="ArrayMesh" id=25] [sub_resource type="CapsuleMesh" id=1] radius = 0.1 @@ -139,6 +140,12 @@ material/0 = null [node name="LeftHand" parent="FPController" instance=ExtResource( 14 )] motion_range = 1 +[node name="Skeleton" parent="FPController/LeftHand/HandModel/Armature001" index="0"] +motion_range = 1 + +[node name="vr_glove_left_slim" parent="FPController/LeftHand/HandModel/Armature001/Skeleton" index="0"] +material/0 = null + [node name="IndexTip" parent="FPController/LeftHand/HandModel/Armature001/Skeleton" index="1"] transform = Transform( 0.19221, -0.669965, -0.717079, 0.977075, 0.19881, 0.076153, 0.0915428, -0.715277, 0.692819, 0.0345973, 0.0355402, -0.164767 ) @@ -151,6 +158,12 @@ material/0 = null motion_range = 1 albedo_texture = ExtResource( 5 ) +[node name="Skeleton" parent="FPController/RightHand/HandModel/Armature" index="0"] +motion_range = 1 + +[node name="vr_glove_right_slim" parent="FPController/RightHand/HandModel/Armature/Skeleton" index="0"] +material/0 = null + [node name="IndexTip" parent="FPController/RightHand/HandModel/Armature/Skeleton" index="1"] transform = Transform( 0.19221, 0.669966, 0.717078, -0.091543, -0.715277, 0.69282, 0.977075, -0.19881, -0.0761527, -0.0345978, -0.164767, -0.0355401 ) @@ -203,6 +216,24 @@ show_target = true active_button = 7 collide_with_areas = true +[node name="HTCTrackerCamera" parent="FPController" instance=ExtResource( 7 )] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0 ) +action = "godot/aim_pose" +path = "/user/vive_tracker_htcx/role/camera" + +[node name="MeshInstance" type="MeshInstance" parent="FPController/HTCTrackerCamera"] +mesh = ExtResource( 25 ) +material/0 = null + +[node name="HTCTrackerKeyboard" parent="FPController" instance=ExtResource( 7 )] +transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0 ) +action = "godot/aim_pose" +path = "/user/vive_tracker_htcx/role/keyboard" + +[node name="MeshInstance" type="MeshInstance" parent="FPController/HTCTrackerKeyboard"] +mesh = ExtResource( 25 ) +material/0 = null + [node name="Table" parent="." instance=ExtResource( 3 )] transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -6 ) diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.json b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.json new file mode 100644 index 0000000..e2ec1cd --- /dev/null +++ b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.json @@ -0,0 +1,37 @@ +{ + "thumbnail": "vive_controller_thumbnail.png", + "components": { + "body": { + "filename": "vr_tracker_vive_3_0.obj", + "component_local": { + "origin": [ 0.0, 0.0, 0.0 ], + "rotate_xyz": [ 0.0, 0.0, 0.0 ] + } + }, + "back": { + "component_local": { + "origin": [ 0.0, 0.0, 0.0 ], + "rotate_xyz": [ 0.0, 0.0, 180.0 ] + } + }, + "pistol": { + "component_local": { + "origin": [ 0.0, 0.0, 0.0 ], + "rotate_xyz": [ -30.0, 0.0, 180.0 ] + } + }, + "front_rolled": { + "component_local": { + "origin": [ 0.0, 0.0, 0.01 ], + "rotate_xyz": [ 90.0, 180.0, 0.0 ] + } + }, + "front": { + "component_local": { + "origin": [ 0.0, 0.0, 0.01 ], + "rotate_xyz": [ 90.0, 0.0, 0.0 ] + } + } + } +} +} diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.mtl b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.mtl new file mode 100644 index 0000000..2c323c8 --- /dev/null +++ b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.mtl @@ -0,0 +1,28 @@ +newmtl VIVE_Tracker_3_0_ +illum 4 +Kd 0.50 0.50 0.50 +Ka 0.00 0.00 0.00 +Tf 1.00 1.00 1.00 +Ni 1.00 +newmtl VIVE_Tracker_CUI__ +illum 4 +Kd 0.50 0.50 0.50 +Ka 0.00 0.00 0.00 +Tf 1.00 1.00 1.00 +Ni 1.00 +newmtl initialShadingGroup +illum 4 +Kd 0.50 0.50 0.50 +Ka 0.00 0.00 0.00 +Tf 1.00 1.00 1.00 +Ni 1.00 +newmtl vr_tracker_vive_3_0_SG +illum 4 +Kd 0.00 0.00 0.00 +Ka 0.00 0.00 0.00 +Tf 1.00 1.00 1.00 +map_Kd vr_tracker_vive_3_0_diff.png +Ni 1.00 +Ks 0.00 0.00 0.00 +map_Ks vr_tracker_vive_3_0_Spec.png +Ns 18.00 diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.obj.import b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.obj.import new file mode 100644 index 0000000..5e10c1c --- /dev/null +++ b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.obj.import @@ -0,0 +1,20 @@ +[remap] + +importer="wavefront_obj" +type="Mesh" +path="res://.import/vr_tracker_vive_3_0.obj-a53b4a08c4f66629a29ada7de74481d0.mesh" + +[deps] + +files=[ "res://.import/vr_tracker_vive_3_0.obj-a53b4a08c4f66629a29ada7de74481d0.mesh" ] + +source_file="res://assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0.obj" +dest_files=[ "res://.import/vr_tracker_vive_3_0.obj-a53b4a08c4f66629a29ada7de74481d0.mesh", "res://.import/vr_tracker_vive_3_0.obj-a53b4a08c4f66629a29ada7de74481d0.mesh" ] + +[params] + +generate_tangents=true +scale_mesh=Vector3( 1, 1, 1 ) +offset_mesh=Vector3( 0, 0, 0 ) +octahedral_compression=true +optimize_mesh_flags=4286 diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png new file mode 100644 index 0000000..538a339 Binary files /dev/null and b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png differ diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png.import b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png.import new file mode 100644 index 0000000..d21411f --- /dev/null +++ b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="StreamTexture" +path.s3tc="res://.import/vr_tracker_vive_3_0_Spec.png-5f0e428f46dc6136a4fa36515f0fab6c.s3tc.stex" +path.etc2="res://.import/vr_tracker_vive_3_0_Spec.png-5f0e428f46dc6136a4fa36515f0fab6c.etc2.stex" +path.etc="res://.import/vr_tracker_vive_3_0_Spec.png-5f0e428f46dc6136a4fa36515f0fab6c.etc.stex" +metadata={ +"imported_formats": [ "s3tc", "etc2", "etc" ], +"vram_texture": true +} + +[deps] + +source_file="res://assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_Spec.png" +dest_files=[ "res://.import/vr_tracker_vive_3_0_Spec.png-5f0e428f46dc6136a4fa36515f0fab6c.s3tc.stex", "res://.import/vr_tracker_vive_3_0_Spec.png-5f0e428f46dc6136a4fa36515f0fab6c.etc2.stex", "res://.import/vr_tracker_vive_3_0_Spec.png-5f0e428f46dc6136a4fa36515f0fab6c.etc.stex" ] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png new file mode 100644 index 0000000..e470243 Binary files /dev/null and b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png differ diff --git a/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png.import b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png.import new file mode 100644 index 0000000..2d11b67 --- /dev/null +++ b/demo/assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="StreamTexture" +path.s3tc="res://.import/vr_tracker_vive_3_0_diff.png-073876868edc19f3fac2656e5573f296.s3tc.stex" +path.etc2="res://.import/vr_tracker_vive_3_0_diff.png-073876868edc19f3fac2656e5573f296.etc2.stex" +path.etc="res://.import/vr_tracker_vive_3_0_diff.png-073876868edc19f3fac2656e5573f296.etc.stex" +metadata={ +"imported_formats": [ "s3tc", "etc2", "etc" ], +"vram_texture": true +} + +[deps] + +source_file="res://assets/htc/vr_tracker_vive_3_0/vr_tracker_vive_3_0_diff.png" +dest_files=[ "res://.import/vr_tracker_vive_3_0_diff.png-073876868edc19f3fac2656e5573f296.s3tc.stex", "res://.import/vr_tracker_vive_3_0_diff.png-073876868edc19f3fac2656e5573f296.etc2.stex", "res://.import/vr_tracker_vive_3_0_diff.png-073876868edc19f3fac2656e5573f296.etc.stex" ] + +[params] + +compress/mode=2 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/demo/scenes/Screen.gd b/demo/scenes/Screen.gd index 2f2a834..3b26a34 100644 --- a/demo/scenes/Screen.gd +++ b/demo/scenes/Screen.gd @@ -26,3 +26,7 @@ func _on_ToggleBounds_pressed(): func _on_TogglePassthrough_pressed(): if fp_controller: fp_controller.enable_passthrough = !fp_controller.enable_passthrough + + +func _on_Quit_pressed(): + get_tree().quit() diff --git a/demo/scenes/Screen.tscn b/demo/scenes/Screen.tscn index dbec3ad..df578df 100644 --- a/demo/scenes/Screen.tscn +++ b/demo/scenes/Screen.tscn @@ -38,9 +38,6 @@ margin_right = 267.0 margin_bottom = 338.0 custom_fonts/font = ExtResource( 1 ) text = "Press me!!" -__meta__ = { -"_edit_use_anchors_": false -} [node name="Count" type="Label" parent="."] margin_left = 290.0 @@ -78,6 +75,15 @@ __meta__ = { "_edit_use_anchors_": false } +[node name="Quit" type="Button" parent="."] +margin_left = 470.0 +margin_top = 347.0 +margin_right = 587.0 +margin_bottom = 385.0 +custom_fonts/font = ExtResource( 1 ) +text = "Quit" + [connection signal="pressed" from="Button" to="." method="_on_Button_pressed"] [connection signal="pressed" from="ToggleBounds" to="." method="_on_ToggleBounds_pressed"] [connection signal="pressed" from="TogglePassthrough" to="." method="_on_TogglePassthrough_pressed"] +[connection signal="pressed" from="Quit" to="." method="_on_Quit_pressed"] diff --git a/src/ARVRInterface.cpp b/src/ARVRInterface.cpp index d77cb13..7e0ea04 100644 --- a/src/ARVRInterface.cpp +++ b/src/ARVRInterface.cpp @@ -9,6 +9,7 @@ #include "openxr/extensions/xr_fb_foveation_extension_wrapper.h" #include "openxr/extensions/xr_fb_passthrough_extension_wrapper.h" #include "openxr/extensions/xr_fb_swapchain_update_state_extension_wrapper.h" +#include "openxr/extensions/xr_htc_vive_tracker_extension_wrapper.h" #include #include @@ -128,6 +129,7 @@ godot_bool godot_arvr_initialize(void *p_data) { arvr_data->openxr_api->register_extension_wrapper(); arvr_data->openxr_api->register_extension_wrapper(); arvr_data->openxr_api->register_extension_wrapper(); + arvr_data->openxr_api->register_extension_wrapper(); // not initialise arvr_data->openxr_api->initialize(); diff --git a/src/gdclasses/OpenXRPose.cpp b/src/gdclasses/OpenXRPose.cpp index 264a008..36e3202 100644 --- a/src/gdclasses/OpenXRPose.cpp +++ b/src/gdclasses/OpenXRPose.cpp @@ -18,6 +18,14 @@ void OpenXRPose::_register_methods() { &OpenXRPose::get_invisible_if_inactive, true); + register_method("get_invisible_if_no_confidence", &OpenXRPose::get_invisible_if_no_confidence); + register_method("set_invisible_if_no_confidence", &OpenXRPose::set_invisible_if_no_confidence); + register_property( + "invisible_if_no_confidence", + &OpenXRPose::set_invisible_if_no_confidence, + &OpenXRPose::get_invisible_if_no_confidence, + true); + // For now these are hard coded based on our actions // As our actions JSON is parsed after initialisation we can't really present the dropdown (yet) // For now this will do @@ -45,7 +53,15 @@ void OpenXRPose::_register_methods() { GODOT_METHOD_RPC_MODE_DISABLED, GODOT_PROPERTY_USAGE_DEFAULT, GODOT_PROPERTY_HINT_ENUM, - "/user/hand/left,/user/hand/right,/user/treadmill"); + "/user/hand/left,/user/hand/right,/user/treadmill" + ",/user/vive_tracker_htcx/role/left_foot,/user/vive_tracker_htcx/role/right_foot" + ",/user/vive_tracker_htcx/role/left_shoulder,/user/vive_tracker_htcx/role/right_shoulder" + ",/user/vive_tracker_htcx/role/left_elbow,/user/vive_tracker_htcx/role/right_elbow" + ",/user/vive_tracker_htcx/role/left_knee,/user/vive_tracker_htcx/role/right_knee" + ",/user/vive_tracker_htcx/role/waist" + ",/user/vive_tracker_htcx/role/chest" + ",/user/vive_tracker_htcx/role/camera" + ",/user/vive_tracker_htcx/role/keyboard"); register_method("is_active", &OpenXRPose::is_active); register_method("get_tracking_confidence", &OpenXRPose::get_tracking_confidence); @@ -119,6 +135,8 @@ bool OpenXRPose::check_action_and_path() { } void OpenXRPose::_physics_process(float delta) { + bool visible = true; + if (openxr_api == nullptr || hand_tracking_wrapper == nullptr) { return; } else if (!openxr_api->is_initialised()) { @@ -126,7 +144,7 @@ void OpenXRPose::_physics_process(float delta) { } if (invisible_if_inactive) { - set_visible(is_active()); + visible = is_active(); } ARVRServer *server = ARVRServer::get_singleton(); @@ -149,7 +167,16 @@ void OpenXRPose::_physics_process(float delta) { Transform t; confidence = _action->get_as_pose(_path, ws, t); set_transform(reference_frame * t); + } else { + // we're not tracking anything... + confidence = TRACKING_CONFIDENCE_NONE; + } + + if (invisible_if_no_confidence && confidence == TRACKING_CONFIDENCE_NONE) { + visible = false; } + + set_visible(visible); } bool OpenXRPose::is_active() { @@ -184,6 +211,14 @@ void OpenXRPose::set_invisible_if_inactive(bool hide) { invisible_if_inactive = hide; } +bool OpenXRPose::get_invisible_if_no_confidence() const { + return invisible_if_no_confidence; +} + +void OpenXRPose::set_invisible_if_no_confidence(bool hide) { + invisible_if_no_confidence = hide; +} + String OpenXRPose::get_action() const { return action; } diff --git a/src/gdclasses/OpenXRPose.h b/src/gdclasses/OpenXRPose.h index 7619fbf..fd54fe7 100644 --- a/src/gdclasses/OpenXRPose.h +++ b/src/gdclasses/OpenXRPose.h @@ -17,6 +17,7 @@ class OpenXRPose : public Spatial { TrackingConfidence confidence = TRACKING_CONFIDENCE_NONE; XRExtHandTrackingExtensionWrapper *hand_tracking_wrapper = nullptr; bool invisible_if_inactive = true; + bool invisible_if_no_confidence = true; String action; String path; @@ -39,6 +40,8 @@ class OpenXRPose : public Spatial { bool get_invisible_if_inactive() const; void set_invisible_if_inactive(bool hide); + bool get_invisible_if_no_confidence() const; + void set_invisible_if_no_confidence(bool hide); String get_action() const; void set_action(const String p_action); diff --git a/src/openxr/OpenXRApi.cpp b/src/openxr/OpenXRApi.cpp index 9862431..838507e 100644 --- a/src/openxr/OpenXRApi.cpp +++ b/src/openxr/OpenXRApi.cpp @@ -52,7 +52,19 @@ const char *OpenXRApi::default_action_sets_json = R"===( "localised_name": "Grip Pose", "paths": [ "/user/hand/left", - "/user/hand/right" + "/user/hand/right", + "/user/vive_tracker_htcx/role/left_foot", + "/user/vive_tracker_htcx/role/right_foot", + "/user/vive_tracker_htcx/role/left_shoulder", + "/user/vive_tracker_htcx/role/right_shoulder", + "/user/vive_tracker_htcx/role/left_elbow", + "/user/vive_tracker_htcx/role/right_elbow", + "/user/vive_tracker_htcx/role/left_knee", + "/user/vive_tracker_htcx/role/right_knee", + "/user/vive_tracker_htcx/role/waist", + "/user/vive_tracker_htcx/role/chest", + "/user/vive_tracker_htcx/role/camera", + "/user/vive_tracker_htcx/role/keyboard" ] }, { @@ -214,7 +226,19 @@ const char *OpenXRApi::default_action_sets_json = R"===( "localised_name": "Controller haptic vibration", "paths": [ "/user/hand/left", - "/user/hand/right" + "/user/hand/right", + "/user/vive_tracker_htcx/role/left_foot", + "/user/vive_tracker_htcx/role/right_foot", + "/user/vive_tracker_htcx/role/left_shoulder", + "/user/vive_tracker_htcx/role/right_shoulder", + "/user/vive_tracker_htcx/role/left_elbow", + "/user/vive_tracker_htcx/role/right_elbow", + "/user/vive_tracker_htcx/role/left_knee", + "/user/vive_tracker_htcx/role/right_knee", + "/user/vive_tracker_htcx/role/waist", + "/user/vive_tracker_htcx/role/chest", + "/user/vive_tracker_htcx/role/camera", + "/user/vive_tracker_htcx/role/keyboard" ] } ] @@ -1290,6 +1314,161 @@ const char *OpenXRApi::default_interaction_profiles_json = R"===( ] } ] + },)===" + R"===({ + "path": "/interaction_profiles/htc/vive_tracker_htcx", + "bindings": [ + { + "set": "godot", + "action": "grip_pose", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/input/grip/pose", + "/user/vive_tracker_htcx/role/right_foot/input/grip/pose", + "/user/vive_tracker_htcx/role/left_shoulder/input/grip/pose", + "/user/vive_tracker_htcx/role/right_shoulder/input/grip/pose", + "/user/vive_tracker_htcx/role/left_elbow/input/grip/pose", + "/user/vive_tracker_htcx/role/right_elbow/input/grip/pose", + "/user/vive_tracker_htcx/role/left_knee/input/grip/pose", + "/user/vive_tracker_htcx/role/right_knee/input/grip/pose", + "/user/vive_tracker_htcx/role/waist/input/grip/pose", + "/user/vive_tracker_htcx/role/chest/input/grip/pose", + "/user/vive_tracker_htcx/role/camera/input/grip/pose", + "/user/vive_tracker_htcx/role/keyboard/input/grip/pose" + ] + }, + { + "set": "godot", + "action": "menu_button", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/input/menu/click", + "/user/vive_tracker_htcx/role/right_foot/input/menu/click", + "/user/vive_tracker_htcx/role/left_shoulder/input/menu/click", + "/user/vive_tracker_htcx/role/right_shoulder/input/menu/click", + "/user/vive_tracker_htcx/role/left_elbow/input/menu/click", + "/user/vive_tracker_htcx/role/right_elbow/input/menu/click", + "/user/vive_tracker_htcx/role/left_knee/input/menu/click", + "/user/vive_tracker_htcx/role/right_knee/input/menu/click", + "/user/vive_tracker_htcx/role/waist/input/menu/click", + "/user/vive_tracker_htcx/role/chest/input/menu/click", + "/user/vive_tracker_htcx/role/camera/input/menu/click", + "/user/vive_tracker_htcx/role/keyboard/input/menu/click", + ] + }, + { + "set": "godot", + "action": "front_button", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/input/trigger/click", + "/user/vive_tracker_htcx/role/right_foot/input/trigger/click", + "/user/vive_tracker_htcx/role/left_shoulder/input/trigger/click", + "/user/vive_tracker_htcx/role/right_shoulder/input/trigger/click", + "/user/vive_tracker_htcx/role/left_elbow/input/trigger/click", + "/user/vive_tracker_htcx/role/right_elbow/input/trigger/click", + "/user/vive_tracker_htcx/role/left_knee/input/trigger/click", + "/user/vive_tracker_htcx/role/right_knee/input/trigger/click", + "/user/vive_tracker_htcx/role/waist/input/trigger/click", + "/user/vive_tracker_htcx/role/chest/input/trigger/click", + "/user/vive_tracker_htcx/role/camera/input/trigger/click", + "/user/vive_tracker_htcx/role/keyboard/input/trigger/click", + ] + }, + { + "set": "godot", + "action": "side_button", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_foot/input/squeeze/click", + "/user/vive_tracker_htcx/role/left_shoulder/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_shoulder/input/squeeze/click", + "/user/vive_tracker_htcx/role/left_elbow/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_elbow/input/squeeze/click", + "/user/vive_tracker_htcx/role/left_knee/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_knee/input/squeeze/click", + "/user/vive_tracker_htcx/role/waist/input/squeeze/click", + "/user/vive_tracker_htcx/role/chest/input/squeeze/click", + "/user/vive_tracker_htcx/role/camera/input/squeeze/click", + "/user/vive_tracker_htcx/role/keyboard/input/squeeze/click", + ] + }, + { + "set": "godot", + "action": "front_trigger", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/input/trigger/value", + "/user/vive_tracker_htcx/role/right_foot/input/trigger/value", + "/user/vive_tracker_htcx/role/left_shoulder/input/trigger/value", + "/user/vive_tracker_htcx/role/right_shoulder/input/trigger/value", + "/user/vive_tracker_htcx/role/left_elbow/input/trigger/value", + "/user/vive_tracker_htcx/role/right_elbow/input/trigger/value", + "/user/vive_tracker_htcx/role/left_knee/input/trigger/value", + "/user/vive_tracker_htcx/role/right_knee/input/trigger/value", + "/user/vive_tracker_htcx/role/waist/input/trigger/value", + "/user/vive_tracker_htcx/role/chest/input/trigger/value", + "/user/vive_tracker_htcx/role/camera/input/trigger/value", + "/user/vive_tracker_htcx/role/keyboard/input/trigger/value", + ] + }, + { + "set": "godot", + "action": "side_trigger", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_foot/input/squeeze/click", + "/user/vive_tracker_htcx/role/left_shoulder/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_shoulder/input/squeeze/click", + "/user/vive_tracker_htcx/role/left_elbow/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_elbow/input/squeeze/click", + "/user/vive_tracker_htcx/role/left_knee/input/squeeze/click", + "/user/vive_tracker_htcx/role/right_knee/input/squeeze/click", + "/user/vive_tracker_htcx/role/waist/input/squeeze/click", + "/user/vive_tracker_htcx/role/chest/input/squeeze/click", + "/user/vive_tracker_htcx/role/camera/input/squeeze/click", + "/user/vive_tracker_htcx/role/keyboard/input/squeeze/click", + ] + }, + { + "set": "godot", + "action": "primary", + "paths": [ + "/user/hand/left/input/trackpad", + "/user/hand/right/input/trackpad" + ] + }, + { + "set": "godot", + "action": "primary_button", + "paths": [ + "/user/hand/left/input/trackpad/click", + "/user/hand/right/input/trackpad/click" + ] + }, + { + "set": "godot", + "action": "primary_touch", + "paths": [ + "/user/hand/left/input/trackpad/touch", + "/user/hand/right/input/trackpad/touch" + ] + }, + { + "set": "godot", + "action": "haptic", + "paths": [ + "/user/vive_tracker_htcx/role/left_foot/output/haptic", + "/user/vive_tracker_htcx/role/right_foot/output/haptic", + "/user/vive_tracker_htcx/role/left_shoulder/output/haptic", + "/user/vive_tracker_htcx/role/right_shoulder/output/haptic", + "/user/vive_tracker_htcx/role/left_elbow/output/haptic", + "/user/vive_tracker_htcx/role/right_elbow/output/haptic", + "/user/vive_tracker_htcx/role/left_knee/output/haptic", + "/user/vive_tracker_htcx/role/right_knee/output/haptic", + "/user/vive_tracker_htcx/role/waist/output/haptic", + "/user/vive_tracker_htcx/role/chest/output/haptic", + "/user/vive_tracker_htcx/role/camera/output/haptic", + "/user/vive_tracker_htcx/role/keyboard/output/haptic" + ] + } + ] } ] )==="; @@ -1309,6 +1488,208 @@ const char *session_states[] = { "XR_SESSION_STATE_EXITING", }; +//////////////////////////////////////////////////////////////////////////////// +// Input + +const char *OpenXRInputBase::get_name() const { + return name; +} + +const XrPath OpenXRInputBase::get_toplevel_path() const { + return toplevel_path; +} + +const XrPath OpenXRInputBase::get_active_profile() const { + return active_profile; +} + +const char *OpenXRInputBase::get_active_profile_name() const { + return active_profile_str; +} + +const int OpenXRInputBase::get_godot_controller() const { + return godot_controller; +} + +const TrackingConfidence OpenXRInputBase::get_tracking_confidence() const { + return tracking_confidence; +} + +OpenXRInputBase::OpenXRInputBase(const char *p_name, godot_int p_hand) { + // Copy our name + int len = strlen(p_name); + if (len > XR_MAX_PATH_LENGTH - 1) { + len = XR_MAX_PATH_LENGTH - 1; + } + memcpy(name, p_name, len); + name[len] = '\0'; + + // Reset our state + hand = p_hand; + toplevel_path = XR_NULL_PATH; + godot_controller = -1; + active_profile = XR_NULL_PATH; + active_profile_str[0] = '\0'; + tracking_confidence = TRACKING_CONFIDENCE_NONE; + + // We create our control controller right away, we want to make sure our controller ids become and stay predictable + if (godot_controller == -1) { + godot_controller = arvr_api->godot_arvr_add_controller(name, p_hand, true, true); + +#ifdef DEBUG + Godot::print("OpenXR mapped {0} to {1}", name, godot_controller); +#endif + } +} + +OpenXRInputBase::~OpenXRInputBase() { +} + +void OpenXRInputBase::set_active_profile(OpenXRApi *p_openxr_api, XrPath p_active_profile) { + if (active_profile == p_active_profile) { + return; + } + + active_profile = p_active_profile; + if (active_profile == XR_NULL_PATH) { + active_profile_str[0] = 0; + Godot::print("OpenXR No interaction profile for {0}", name); + return; + } + + uint32_t strl; + XrResult res = xrPathToString(p_openxr_api->get_instance(), active_profile, XR_MAX_PATH_LENGTH, &strl, active_profile_str); + if (!p_openxr_api->xr_result(res, "Failed to get interaction profile path str for {0}", name)) { + return; + } + + Godot::print("OpenXR Event: Interaction profile changed for {0}: {1}", name, active_profile_str); +} + +void OpenXRInputBase::bind(OpenXRApi *p_openxr_api) { + XrResult res = xrStringToPath(p_openxr_api->get_instance(), name, &toplevel_path); + p_openxr_api->xr_result(res, "OpenXR Couldn't obtain path for {0}", name); +} + +void OpenXRInputBase::unbind(OpenXRApi *p_openxr_api) { + toplevel_path = XR_NULL_PATH; + active_profile = XR_NULL_PATH; + if (godot_controller >= 0) { + arvr_api->godot_arvr_remove_controller(godot_controller); + godot_controller = -1; + } +} + +OpenXRInputController::OpenXRInputController(const char *p_name, godot_int p_hand) : OpenXRInputBase(p_name, p_hand) { + // nothing to do here for now... +} + +void OpenXRInputController::update(OpenXRApi *p_openxr_api) { + if (toplevel_path == XR_NULL_PATH) { + // no path, skip this + } else if (godot_controller == -1) { + // not Godot controller registered + } else { + const float ws = ARVRServer::get_singleton()->get_world_scale(); + OpenXRApi::DefaultAction *default_actions = &p_openxr_api->default_actions[0]; // Copy for easy access + bool is_active = false; + + // If our aim pose is active, our controller is active + // note, if the user has removed this action then our old controller approach becomes defunct + if (default_actions[OpenXRApi::ACTION_AIM_POSE].action != NULL) { + is_active = default_actions[OpenXRApi::ACTION_AIM_POSE].action->is_pose_active(toplevel_path); + } + + if (is_active) { + // Start with our pose, we put our ARVRController on our aim pose (may need to change this to our grip pose...) + godot_transform controller_transform; + Transform *t = (Transform *)&controller_transform; + tracking_confidence = default_actions[OpenXRApi::ACTION_AIM_POSE].action->get_as_pose(toplevel_path, ws, *t); + + if (tracking_confidence != TRACKING_CONFIDENCE_NONE) { + arvr_api->godot_arvr_set_controller_transform(godot_controller, &controller_transform, true, true); + } + + // OK, so OpenXR will tell us if the value has changed and we could skip sending our value + // but Godot also checks it so... just let Godot do it + + // Button and axis are hardcoded.. + // Axis + if (default_actions[OpenXRApi::ACTION_FRONT_TRIGGER].action != NULL) { + arvr_api->godot_arvr_set_controller_axis(godot_controller, 2, default_actions[OpenXRApi::ACTION_FRONT_TRIGGER].action->get_as_float(toplevel_path), true); // 0.0 -> 1.0 + } + if (default_actions[OpenXRApi::ACTION_SIDE_TRIGGER].action != NULL) { + arvr_api->godot_arvr_set_controller_axis(godot_controller, 4, default_actions[OpenXRApi::ACTION_SIDE_TRIGGER].action->get_as_float(toplevel_path), true); // 0.0 -> 1.0 + } + if (default_actions[OpenXRApi::ACTION_PRIMARY].action != NULL) { + Vector2 v = default_actions[OpenXRApi::ACTION_PRIMARY].action->get_as_vector(toplevel_path); + arvr_api->godot_arvr_set_controller_axis(godot_controller, 0, v.x, true); // -1.0 -> 1.0 + arvr_api->godot_arvr_set_controller_axis(godot_controller, 1, v.y, true); // -1.0 -> 1.0 + } + if (default_actions[OpenXRApi::ACTION_SECONDARY].action != NULL) { + Vector2 v = default_actions[OpenXRApi::ACTION_SECONDARY].action->get_as_vector(toplevel_path); + arvr_api->godot_arvr_set_controller_axis(godot_controller, 6, v.x, true); // -1.0 -> 1.0 + arvr_api->godot_arvr_set_controller_axis(godot_controller, 7, v.y, true); // -1.0 -> 1.0 + } + // Buttons + if (default_actions[OpenXRApi::ACTION_AX_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 7, default_actions[OpenXRApi::ACTION_AX_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_BY_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 1, default_actions[OpenXRApi::ACTION_BY_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_AX_TOUCH].action != nullptr) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 5, default_actions[OpenXRApi::ACTION_AX_TOUCH].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_BY_TOUCH].action != nullptr) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 6, default_actions[OpenXRApi::ACTION_BY_TOUCH].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_MENU_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 3, default_actions[OpenXRApi::ACTION_MENU_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_SELECT_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 4, default_actions[OpenXRApi::ACTION_SELECT_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_FRONT_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 15, default_actions[OpenXRApi::ACTION_FRONT_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_FRONT_TOUCH].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 16, default_actions[OpenXRApi::ACTION_FRONT_TOUCH].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_SIDE_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 2, default_actions[OpenXRApi::ACTION_SIDE_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_PRIMARY_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 14, default_actions[OpenXRApi::ACTION_PRIMARY_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_SECONDARY_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 13, default_actions[OpenXRApi::ACTION_SECONDARY_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_PRIMARY_TOUCH].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 12, default_actions[OpenXRApi::ACTION_PRIMARY_TOUCH].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_SECONDARY_TOUCH].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 11, default_actions[OpenXRApi::ACTION_SECONDARY_TOUCH].action->get_as_bool(toplevel_path)); + } + + if (default_actions[OpenXRApi::ACTION_HAPTIC].action != NULL) { + // Godot currently only gives us a float between 0.0 and 1.0 for rumble strength. + // Full haptic control will be offered through another object + float haptic = arvr_api->godot_arvr_get_controller_rumble(godot_controller); + if (haptic > 0.0) { + // 17,000,000.0 nanoseconds (17ms) is slightly more then the duration of one frame if we're outputting at 60fps + // so if we sustain our pulse we should be issuing a new pulse before the old one ends + default_actions[OpenXRApi::ACTION_HAPTIC].action->do_haptic_pulse(toplevel_path, 17.0 * 1000 * 1000, XR_FREQUENCY_UNSPECIFIED, haptic); + } + } + } else { + // In Godot 4 this will also mark the tracker as inactive but in Godot 3 we'll need the user to check this manually + // But our old approach of removing and re-adding controllers means that IDs get messed up so... + tracking_confidence = TRACKING_CONFIDENCE_NONE; + } + } +} + //////////////////////////////////////////////////////////////////////////////// // Singleton management @@ -2182,6 +2563,28 @@ void OpenXRApi::cleanupSwapChains() { } } +void OpenXRApi::add_input_map(OpenXRInputBase *p_input_map) { + inputmaps.push_back(p_input_map); +} + +void OpenXRApi::setup_input_maps() { + add_input_map(new OpenXRInputController("/user/hand/left", 1)); + add_input_map(new OpenXRInputController("/user/hand/right", 2)); + + for (XRExtensionWrapper *wrapper : registered_extension_wrappers) { + wrapper->add_input_maps(); + } +} + +void OpenXRApi::cleanup_input_maps() { + // unbindActionSets should already have been called so we can just clear our map + for (uint64_t i = 0; i < inputmaps.size(); i++) { + OpenXRInputBase *inputmap = inputmaps[i]; + delete inputmap; + } + inputmaps.clear(); +} + bool OpenXRApi::loadActionSets() { #ifdef DEBUG Godot::print("OpenXR loadActionSets"); @@ -2212,9 +2615,8 @@ bool OpenXRApi::bindActionSets() { // a developer that is not using the internal actions but defines their own may not care about these missing // Init our input paths and godot controllers for our mapping to - for (uint64_t i = 0; i < USER_INPUT_MAX; i++) { - XrResult res = xrStringToPath(instance, inputmaps[i].name, &inputmaps[i].toplevel_path); - xr_result(res, "OpenXR Couldn't obtain path for {0}", inputmaps[i].name); + for (uint64_t i = 0; i < inputmaps.size(); i++) { + inputmaps[i]->bind(this); } // find our default actions @@ -2230,13 +2632,8 @@ bool OpenXRApi::bindActionSets() { void OpenXRApi::unbindActionSets() { // cleanup our controller mapping - for (uint64_t i = 0; i < USER_INPUT_MAX; i++) { - inputmaps[i].toplevel_path = XR_NULL_PATH; - inputmaps[i].active_profile = XR_NULL_PATH; - if (inputmaps[i].godot_controller >= 0) { - arvr_api->godot_arvr_remove_controller(inputmaps[i].godot_controller); - inputmaps[i].godot_controller = -1; - } + for (uint64_t i = 0; i < inputmaps.size(); i++) { + inputmaps[i]->unbind(this); } // reset our default actions @@ -2423,6 +2820,7 @@ bool OpenXRApi::on_state_ready() { initialiseSpaces(); initialiseSwapChains(); + setup_input_maps(); bindActionSets(); running = true; @@ -2506,6 +2904,7 @@ bool OpenXRApi::on_state_stopping() { // need to cleanup various things which would otherwise be re-allocated if we have a state change back to ready // note that cleaning up our action sets will invalidate many of the OpenXR nodes so we need to improve that as well. unbindActionSets(); + cleanup_input_maps(); cleanupSwapChains(); cleanupSpaces(); @@ -2594,8 +2993,10 @@ godot::Array OpenXRApi::get_enabled_extensions() const { } bool OpenXRApi::is_input_map_controller(int p_godot_controller) { + // TODO, this needs to change because this now always returns true as all our controllers are registered here now + // Possibly we should add a type variable and just return that. for (const auto &inputmap : inputmaps) { - if (inputmap.godot_controller == p_godot_controller) { + if (inputmap->get_godot_controller() == p_godot_controller) { return true; } } @@ -2604,8 +3005,8 @@ bool OpenXRApi::is_input_map_controller(int p_godot_controller) { TrackingConfidence OpenXRApi::get_controller_tracking_confidence(const int p_godot_controller) const { for (const auto &inputmap : inputmaps) { - if (inputmap.godot_controller == p_godot_controller) { - return inputmap.tracking_confidence; + if (inputmap->get_godot_controller() == p_godot_controller) { + return inputmap->get_tracking_confidence(); } } @@ -3173,121 +3574,8 @@ void OpenXRApi::update_actions() { // now loop through our controllers, updated our positional trackers // and perform our backwards compatibility layer - const float ws = ARVRServer::get_singleton()->get_world_scale(); - - for (uint64_t i = 0; i < USER_INPUT_MAX; i++) { - XrPath input_path = inputmaps[i].toplevel_path; - if (input_path == XR_NULL_PATH) { - // no path, skip this - } else { - bool is_active = false; - - // If our aim pose is active, our controller is active - // note, if the user has removed this action then our old controller approach becomes defunct - if (default_actions[ACTION_AIM_POSE].action != NULL) { - is_active = default_actions[ACTION_AIM_POSE].action->is_pose_active(input_path); - } - - if (is_active) { - if (inputmaps[i].godot_controller == -1) { - // hate using const_cast here but godot_arvr_add_controller should have it's parameter defined as const, it doesn't change it... - inputmaps[i].godot_controller = arvr_api->godot_arvr_add_controller(const_cast(inputmaps[i].name), (godot_int)i + 1, true, true); - -#ifdef DEBUG - Godot::print("OpenXR mapped {0} to {1}", inputmaps[i].name, inputmaps[i].godot_controller); -#endif - } - - // copy for readability - int godot_controller = inputmaps[i].godot_controller; - - // Start with our pose, we put our ARVRController on our aim pose (may need to change this to our grip pose...) - godot_transform controller_transform; - Transform *t = (Transform *)&controller_transform; - inputmaps[i].tracking_confidence = default_actions[ACTION_AIM_POSE].action->get_as_pose(input_path, ws, *t); - - if (inputmaps[i].tracking_confidence != TRACKING_CONFIDENCE_NONE) { - arvr_api->godot_arvr_set_controller_transform(godot_controller, &controller_transform, true, true); - } - - // OK, so OpenXR will tell us if the value has changed and we could skip sending our value - // but Godot also checks it so... just let Godot do it - - // Button and axis are hardcoded.. - // Axis - if (default_actions[ACTION_FRONT_TRIGGER].action != NULL) { - arvr_api->godot_arvr_set_controller_axis(godot_controller, 2, default_actions[ACTION_FRONT_TRIGGER].action->get_as_float(input_path), true); // 0.0 -> 1.0 - } - if (default_actions[ACTION_SIDE_TRIGGER].action != NULL) { - arvr_api->godot_arvr_set_controller_axis(godot_controller, 4, default_actions[ACTION_SIDE_TRIGGER].action->get_as_float(input_path), true); // 0.0 -> 1.0 - } - if (default_actions[ACTION_PRIMARY].action != NULL) { - Vector2 v = default_actions[ACTION_PRIMARY].action->get_as_vector(input_path); - arvr_api->godot_arvr_set_controller_axis(godot_controller, 0, v.x, true); // -1.0 -> 1.0 - arvr_api->godot_arvr_set_controller_axis(godot_controller, 1, v.y, true); // -1.0 -> 1.0 - } - if (default_actions[ACTION_SECONDARY].action != NULL) { - Vector2 v = default_actions[ACTION_SECONDARY].action->get_as_vector(input_path); - arvr_api->godot_arvr_set_controller_axis(godot_controller, 6, v.x, true); // -1.0 -> 1.0 - arvr_api->godot_arvr_set_controller_axis(godot_controller, 7, v.y, true); // -1.0 -> 1.0 - } - // Buttons - if (default_actions[ACTION_AX_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 7, default_actions[ACTION_AX_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_BY_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 1, default_actions[ACTION_BY_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_AX_TOUCH].action != nullptr) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 5, default_actions[ACTION_AX_TOUCH].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_BY_TOUCH].action != nullptr) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 6, default_actions[ACTION_BY_TOUCH].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_MENU_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 3, default_actions[ACTION_MENU_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_SELECT_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 4, default_actions[ACTION_SELECT_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_FRONT_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 15, default_actions[ACTION_FRONT_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_FRONT_TOUCH].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 16, default_actions[ACTION_FRONT_TOUCH].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_SIDE_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 2, default_actions[ACTION_SIDE_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_PRIMARY_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 14, default_actions[ACTION_PRIMARY_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_SECONDARY_BUTTON].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 13, default_actions[ACTION_SECONDARY_BUTTON].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_PRIMARY_TOUCH].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 12, default_actions[ACTION_PRIMARY_TOUCH].action->get_as_bool(input_path)); - } - if (default_actions[ACTION_SECONDARY_TOUCH].action != NULL) { - arvr_api->godot_arvr_set_controller_button(godot_controller, 11, default_actions[ACTION_SECONDARY_TOUCH].action->get_as_bool(input_path)); - } - - if (default_actions[ACTION_HAPTIC].action != NULL) { - // Godot currently only gives us a float between 0.0 and 1.0 for rumble strength. - // Full haptic control will be offered through another object - float haptic = arvr_api->godot_arvr_get_controller_rumble(godot_controller); - if (haptic > 0.0) { - // 17,000,000.0 nanoseconds (17ms) is slightly more then the duration of one frame if we're outputting at 60fps - // so if we sustain our pulse we should be issuing a new pulse before the old one ends - default_actions[ACTION_HAPTIC].action->do_haptic_pulse(input_path, 17.0 * 1000 * 1000, XR_FREQUENCY_UNSPECIFIED, haptic); - } - } - } else if (inputmaps[i].godot_controller != -1) { - // Remove our controller, it's no longer active - arvr_api->godot_arvr_remove_controller(inputmaps[i].godot_controller); - inputmaps[i].godot_controller = -1; - } - } + for (uint64_t i = 0; i < inputmaps.size(); i++) { + inputmaps[i]->update(this); } } @@ -3547,40 +3835,20 @@ bool OpenXRApi::poll_events() { .next = nullptr }; - for (int i = 0; i < USER_INPUT_MAX; i++) { - XrPath input_path = inputmaps[i].toplevel_path; + for (int i = 0; i < inputmaps.size(); i++) { + XrPath input_path = inputmaps[i]->get_toplevel_path(); if (input_path == XR_NULL_PATH) { // incorrect path continue; } XrResult res = xrGetCurrentInteractionProfile(event->session, input_path, &profile_state); - if (!xr_result(res, "Failed to get interaction profile for {0}", inputmaps[i].name)) { + if (!xr_result(res, "Failed to get interaction profile for {0}", inputmaps[i]->get_name())) { continue; } - XrPath new_profile = profile_state.interactionProfile; - if (inputmaps[i].active_profile != new_profile) { - inputmaps[i].active_profile = new_profile; - if (new_profile == XR_NULL_PATH) { - Godot::print("OpenXR No interaction profile for {0}", inputmaps[i].name); - continue; - } - - uint32_t strl; - char profile_str[XR_MAX_PATH_LENGTH]; - res = xrPathToString(instance, new_profile, XR_MAX_PATH_LENGTH, &strl, profile_str); - if (!xr_result(res, "Failed to get interaction profile path str for {0}", inputmaps[i].name)) { - continue; - } - -#ifdef DEBUG - Godot::print("OpenXR Event: Interaction profile changed for {0}: {1}", inputmaps[i].name, profile_str); -#endif - } + inputmaps[i]->set_active_profile(this, profile_state.interactionProfile); } - - // TODO: do something } break; default: if (!handled) { diff --git a/src/openxr/OpenXRApi.h b/src/openxr/OpenXRApi.h index 96bd177..142a603 100644 --- a/src/openxr/OpenXRApi.h +++ b/src/openxr/OpenXRApi.h @@ -61,10 +61,47 @@ enum TrackingConfidence { #include "openxr/actions/action.h" #include "openxr/actions/actionset.h" -#define USER_INPUT_MAX 2 - using namespace godot; +class OpenXRApi; + +class OpenXRInputBase { +protected: + godot_int hand; // 0 = not applicable, 1 = left hand controller, 2 = right hand controller + char name[XR_MAX_PATH_LENGTH]; + XrPath toplevel_path; + XrPath active_profile; // note, this can be a profile added in the OpenXR runtime unknown to our default mappings + char active_profile_str[XR_MAX_PATH_LENGTH]; + + TrackingConfidence tracking_confidence; + godot_int godot_controller; + +public: + // these are mostly all read only from outside + const char *get_name() const; + const XrPath get_toplevel_path() const; + const XrPath get_active_profile() const; + const char *get_active_profile_name() const; + const int get_godot_controller() const; + const TrackingConfidence get_tracking_confidence() const; + + // interface + void set_active_profile(OpenXRApi *p_openxr_api, XrPath p_active_profile); + virtual void bind(OpenXRApi *p_openxr_api); + virtual void unbind(OpenXRApi *p_openxr_api); + virtual void update(OpenXRApi *p_openxr_api) = 0; + + OpenXRInputBase(const char *p_name, godot_int p_hand = 0); + virtual ~OpenXRInputBase(); +}; + +class OpenXRInputController : public OpenXRInputBase { +public: + virtual void update(OpenXRApi *p_openxr_api) override; + + OpenXRInputController(const char *p_name, godot_int p_hand = 0); +}; + class OpenXRApi { friend class Action; friend class ActionSet; @@ -73,22 +110,7 @@ class OpenXRApi { // These are hardcoded and meant for our backwards compatibility layer // If not configured in our action sets they will be defunct - struct InputMap { - const char *name; - XrPath toplevel_path; - godot_int godot_controller; - XrPath active_profile; // note, this can be a profile added in the OpenXR runtime unknown to our default mappings - TrackingConfidence tracking_confidence; - }; - - InputMap inputmaps[USER_INPUT_MAX] = { - { "/user/hand/left", XR_NULL_PATH, -1, XR_NULL_PATH, TRACKING_CONFIDENCE_NONE }, - { "/user/hand/right", XR_NULL_PATH, -1, XR_NULL_PATH, TRACKING_CONFIDENCE_NONE }, - // gamepad is already supported in Godots own joystick handling, head we're using directly - // { "/user/foot/left", XR_NULL_PATH, -1, XR_NULL_PATH, TRACKING_CONFIDENCE_NONE }, - // { "/user/foot/right", XR_NULL_PATH, -1, XR_NULL_PATH, TRACKING_CONFIDENCE_NONE }, - // { "/user/treadmill", XR_NULL_PATH, -1, XR_NULL_PATH, TRACKING_CONFIDENCE_NONE }, - }; + std::vector inputmaps; // Default actions we support so we can mimic our old ARVRController handling enum DefaultActions { @@ -250,6 +272,9 @@ class OpenXRApi { bool initialiseSwapChains(); void cleanupSwapChains(); + void setup_input_maps(); + void cleanup_input_maps(); + bool loadActionSets(); bool bindActionSets(); void unbindActionSets(); @@ -360,6 +385,8 @@ class OpenXRApi { TrackingConfidence get_controller_tracking_confidence(const int p_godot_controller) const; + void add_input_map(OpenXRInputBase *p_input_map); + static const char *default_action_sets_json; godot::String get_action_sets_json() const; void set_action_sets_json(const godot::String &p_action_sets_json); diff --git a/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.cpp b/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.cpp index fef46fc..3a7b5a5 100644 --- a/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.cpp +++ b/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.cpp @@ -4,11 +4,187 @@ #include "openxr/include/util.h" +HandTracker::HandTracker(XRExtHandTrackingExtensionWrapper *p_hand_tracking_api, const char *p_name, XrHandEXT p_hand_ext) : OpenXRInputBase(p_name, 0) { + is_initialised = false; + hand_tracker = XR_NULL_HANDLE; + hand_tracking_api = p_hand_tracking_api; + hand_ext = p_hand_ext; +} + +void HandTracker::update(OpenXRApi *p_openxr_api) { + if (!hand_tracking_api) { + return; + } + if (!hand_tracking_api->is_supported()) { + return; + } + + const XrTime time = p_openxr_api->get_next_frame_time(); // This data will be used for the next frame we render + + XrResult result; + + if (hand_tracker == XR_NULL_HANDLE) { + XrHandTrackerCreateInfoEXT createInfo = { + .type = XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT, + .next = nullptr, + .hand = hand_ext, + .handJointSet = XR_HAND_JOINT_SET_DEFAULT_EXT, + }; + + result = hand_tracking_api->xrCreateHandTrackerEXT(p_openxr_api->get_session(), &createInfo, &hand_tracker); + if (!p_openxr_api->xr_result(result, "Failed to obtain hand tracking information")) { + // not successful? then we do nothing. + is_initialised = false; + } else { + velocities = { + .type = XR_TYPE_HAND_JOINT_VELOCITIES_EXT, + .jointCount = XR_HAND_JOINT_COUNT_EXT, + .jointVelocities = joint_velocities, + }; + + if (hand_tracking_api->tracking_aim_state_supported()) { + aimState = { + .type = XR_TYPE_HAND_TRACKING_AIM_STATE_FB, + .next = nullptr, + }; + + velocities.next = &aimState; + } + + locations = { + .type = XR_TYPE_HAND_JOINT_LOCATIONS_EXT, + .next = &velocities, + .isActive = false, + .jointCount = XR_HAND_JOINT_COUNT_EXT, + .jointLocations = joint_locations, + }; + + is_initialised = true; + } + } + + if (is_initialised) { + void *next_pointer = nullptr; + + XrHandJointsMotionRangeInfoEXT motionRangeInfo; + + if (hand_tracking_api->hand_motion_supported()) { + motionRangeInfo = { + .type = XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT, + .next = next_pointer, + .handJointsMotionRange = motion_range, + }; + + next_pointer = &motionRangeInfo; + } + + XrHandJointsLocateInfoEXT locateInfo = { + .type = XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT, + .next = next_pointer, + .baseSpace = p_openxr_api->get_play_space(), + .time = time, + }; + + result = hand_tracking_api->xrLocateHandJointsEXT(hand_tracker, &locateInfo, &locations); + if (!p_openxr_api->xr_result(result, "failed to get tracking for hand {0}!", name)) { + return; + } + + // For some reason an inactive controller isn't coming back as inactive but has coordinates either as NAN or very large + const XrPosef &palm = joint_locations[XR_HAND_JOINT_PALM_EXT].pose; + if (!locations.isActive || isnan(palm.position.x) || palm.position.x < -1000000.00 || palm.position.x > 1000000.00) { + locations.isActive = false; // workaround, make sure its inactive + } + + if (hand_tracking_api->tracking_aim_state_supported() && locations.isActive && godot_controller != -1) { + tracking_confidence = check_bit(XR_HAND_TRACKING_AIM_VALID_BIT_FB, aimState.status) ? TrackingConfidence::TRACKING_CONFIDENCE_HIGH : TrackingConfidence::TRACKING_CONFIDENCE_NONE; + + const float ws = ARVRServer::get_singleton()->get_world_scale(); + godot_transform controller_transform; + auto *t = (Transform *)&controller_transform; + + *t = p_openxr_api->transform_from_pose(aimState.aimPose, ws); + arvr_api->godot_arvr_set_controller_transform( + godot_controller, + &controller_transform, + true, + true); + + // Index pinch is mapped to the A/X button + arvr_api->godot_arvr_set_controller_button( + godot_controller, + 7, + check_bit(XR_HAND_TRACKING_AIM_INDEX_PINCHING_BIT_FB, aimState.status)); + // Middle pinch is mapped to the B/Y button + arvr_api->godot_arvr_set_controller_button( + godot_controller, + 1, + check_bit(XR_HAND_TRACKING_AIM_MIDDLE_PINCHING_BIT_FB, aimState.status)); + // Ring pinch is mapped to the front trigger + arvr_api->godot_arvr_set_controller_button( + godot_controller, + 15, + check_bit(XR_HAND_TRACKING_AIM_RING_PINCHING_BIT_FB, aimState.status)); + // Little finger pinch is mapped to the side trigger / grip button + arvr_api->godot_arvr_set_controller_button( + godot_controller, + 2, + check_bit(XR_HAND_TRACKING_AIM_LITTLE_PINCHING_BIT_FB, aimState.status)); + // Menu button + arvr_api->godot_arvr_set_controller_button( + godot_controller, + 3, + check_bit(XR_HAND_TRACKING_AIM_MENU_PRESSED_BIT_FB, aimState.status)); + + // To allow accessing the pinch state as provided by the API we map them here to + // the joystick axis of the controller. This will give the ability to access the + // basic hand tracking gestures without the need to query specific APIs. + arvr_api->godot_arvr_set_controller_axis( + godot_controller, + 7, + aimState.pinchStrengthIndex, + false); + arvr_api->godot_arvr_set_controller_axis( + godot_controller, + 6, + aimState.pinchStrengthMiddle, + false); + arvr_api->godot_arvr_set_controller_axis( + godot_controller, + 2, + aimState.pinchStrengthRing, + false); + arvr_api->godot_arvr_set_controller_axis( + godot_controller, + 4, + aimState.pinchStrengthLittle, + false); + + } else { + tracking_confidence = TrackingConfidence::TRACKING_CONFIDENCE_NONE; + } + } +} + +void HandTracker::cleanup() { + // May more this into unbind, need to see if that is called at the right times.. + if (hand_tracker != XR_NULL_HANDLE) { + hand_tracking_api->xrDestroyHandTrackerEXT(hand_tracker); + hand_tracker = XR_NULL_HANDLE; + } + + is_initialised = false; +} + XRExtHandTrackingExtensionWrapper::XRExtHandTrackingExtensionWrapper() { openxr_api = OpenXRApi::openxr_get_api(); request_extensions[XR_EXT_HAND_TRACKING_EXTENSION_NAME] = &hand_tracking_ext; request_extensions[XR_EXT_HAND_JOINTS_MOTION_RANGE_EXTENSION_NAME] = &hand_motion_range_ext; request_extensions[XR_FB_HAND_TRACKING_AIM_EXTENSION_NAME] = &hand_tracking_aim_state_ext; + + for (int i = 0; i < MAX_TRACKED_HANDS; i++) { + hand_trackers[i] = nullptr; + } } XRExtHandTrackingExtensionWrapper::~XRExtHandTrackingExtensionWrapper() { @@ -35,6 +211,14 @@ XRExtHandTrackingExtensionWrapper *XRExtHandTrackingExtensionWrapper::get_single return singleton; } +void XRExtHandTrackingExtensionWrapper::add_input_maps() { + // This should register our trackers in slots 3 and 4 as long as our extensions are registered in the correct order. + for (int i = 0; i < MAX_TRACKED_HANDS; i++) { + hand_trackers[i] = new HandTracker(this, hand_controller_names[i], i == 0 ? XR_HAND_LEFT_EXT : XR_HAND_RIGHT_EXT); + openxr_api->add_input_map(hand_trackers[i]); + } +} + void XRExtHandTrackingExtensionWrapper::on_instance_initialized(const XrInstance instance) { if (hand_tracking_ext) { XrResult result = initialise_ext_hand_tracking_extension(instance); @@ -48,10 +232,6 @@ void XRExtHandTrackingExtensionWrapper::on_state_ready() { initialize_hand_tracking(); } -void XRExtHandTrackingExtensionWrapper::on_process_openxr() { - update_handtracking(); -} - void XRExtHandTrackingExtensionWrapper::on_state_stopping() { cleanup_hand_tracking(); } @@ -149,14 +329,6 @@ bool XRExtHandTrackingExtensionWrapper::initialize_hand_tracking() { return false; } - for (int i = 0; i < 2; i++) { - // we'll do this later - hand_trackers[i].is_initialised = false; - hand_trackers[i].hand_tracker = XR_NULL_HANDLE; - hand_trackers[i].aim_state_godot_controller = -1; - hand_trackers[i].tracking_confidence = TrackingConfidence::TRACKING_CONFIDENCE_NONE; - } - #ifdef DEBUG Godot::print("Hand tracking is supported\n"); #endif @@ -166,29 +338,22 @@ bool XRExtHandTrackingExtensionWrapper::initialize_hand_tracking() { } void XRExtHandTrackingExtensionWrapper::cleanup_hand_tracking() { - for (int i = 0; i < 2; i++) { - if (hand_trackers[i].hand_tracker != XR_NULL_HANDLE) { - xrDestroyHandTrackerEXT(hand_trackers[i].hand_tracker); - - hand_trackers[i].is_initialised = false; - hand_trackers[i].hand_tracker = XR_NULL_HANDLE; - hand_trackers[i].aim_state_godot_controller = -1; - hand_trackers[i].tracking_confidence = TrackingConfidence::TRACKING_CONFIDENCE_NONE; - } + for (int i = 0; i < MAX_TRACKED_HANDS; i++) { + hand_trackers[i]->cleanup(); } } const HandTracker *XRExtHandTrackingExtensionWrapper::get_hand_tracker(uint32_t p_hand) const { if (p_hand < MAX_TRACKED_HANDS) { - return &hand_trackers[p_hand]; + return hand_trackers[p_hand]; } else { return nullptr; } } XrHandJointsMotionRangeEXT XRExtHandTrackingExtensionWrapper::get_motion_range(uint32_t p_hand) const { - if (p_hand < MAX_TRACKED_HANDS) { - return hand_trackers[p_hand].motion_range; + if (p_hand < MAX_TRACKED_HANDS && hand_trackers[p_hand]) { + return hand_trackers[p_hand]->motion_range; } else { // just return this as the default return XR_HAND_JOINTS_MOTION_RANGE_UNOBSTRUCTED_EXT; @@ -196,14 +361,14 @@ XrHandJointsMotionRangeEXT XRExtHandTrackingExtensionWrapper::get_motion_range(u } void XRExtHandTrackingExtensionWrapper::set_motion_range(uint32_t p_hand, XrHandJointsMotionRangeEXT p_motion_range) { - if (p_hand < MAX_TRACKED_HANDS) { - hand_trackers[p_hand].motion_range = p_motion_range; + if (p_hand < MAX_TRACKED_HANDS && hand_trackers[p_hand]) { + hand_trackers[p_hand]->motion_range = p_motion_range; } } bool XRExtHandTrackingExtensionWrapper::is_hand_tracker_controller(const int p_godot_controller) { for (const auto &hand_tracker : hand_trackers) { - if (hand_tracker.aim_state_godot_controller == p_godot_controller) { + if (hand_tracker && hand_tracker->get_godot_controller() == p_godot_controller) { return true; } } @@ -212,179 +377,10 @@ bool XRExtHandTrackingExtensionWrapper::is_hand_tracker_controller(const int p_g TrackingConfidence XRExtHandTrackingExtensionWrapper::get_hand_tracker_tracking_confidence(const int p_godot_controller) { for (const auto &hand_tracker : hand_trackers) { - if (hand_tracker.aim_state_godot_controller == p_godot_controller) { - return hand_tracker.tracking_confidence; + if (hand_tracker && hand_tracker->get_godot_controller() == p_godot_controller) { + return hand_tracker->get_tracking_confidence(); } } return TRACKING_CONFIDENCE_NONE; } -void XRExtHandTrackingExtensionWrapper::update_handtracking() { - if (!hand_tracking_supported) { - return; - } - - const XrTime time = openxr_api->get_next_frame_time(); // This data will be used for the next frame we render - - XrResult result; - - for (int i = 0; i < 2; i++) { - if (hand_trackers[i].hand_tracker == XR_NULL_HANDLE) { - XrHandTrackerCreateInfoEXT createInfo = { - .type = XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT, - .next = nullptr, - .hand = i == 0 ? XR_HAND_LEFT_EXT : XR_HAND_RIGHT_EXT, - .handJointSet = XR_HAND_JOINT_SET_DEFAULT_EXT, - }; - - result = xrCreateHandTrackerEXT(openxr_api->get_session(), &createInfo, &hand_trackers[i].hand_tracker); - if (!openxr_api->xr_result(result, "Failed to obtain hand tracking information")) { - // not successful? then we do nothing. - hand_trackers[i].is_initialised = false; - } else { - hand_trackers[i].velocities = { - .type = XR_TYPE_HAND_JOINT_VELOCITIES_EXT, - .jointCount = XR_HAND_JOINT_COUNT_EXT, - .jointVelocities = hand_trackers[i].joint_velocities, - }; - - if (hand_tracking_aim_state_ext) { - hand_trackers[i].aimState = { - .type = XR_TYPE_HAND_TRACKING_AIM_STATE_FB, - .next = nullptr, - }; - - hand_trackers[i].velocities.next = &hand_trackers[i].aimState; - } - - hand_trackers[i].locations = { - .type = XR_TYPE_HAND_JOINT_LOCATIONS_EXT, - .next = &hand_trackers[i].velocities, - .isActive = false, - .jointCount = XR_HAND_JOINT_COUNT_EXT, - .jointLocations = hand_trackers[i].joint_locations, - }; - - hand_trackers[i].is_initialised = true; - } - } - - if (hand_trackers[i].is_initialised) { - void *next_pointer = nullptr; - - XrHandJointsMotionRangeInfoEXT motionRangeInfo; - - if (hand_motion_range_ext) { - motionRangeInfo = { - .type = XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT, - .next = next_pointer, - .handJointsMotionRange = hand_trackers[i].motion_range, - }; - - next_pointer = &motionRangeInfo; - } - - XrHandJointsLocateInfoEXT locateInfo = { - .type = XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT, - .next = next_pointer, - .baseSpace = openxr_api->get_play_space(), - .time = time, - }; - - result = xrLocateHandJointsEXT(hand_trackers[i].hand_tracker, &locateInfo, &hand_trackers[i].locations); - if (!openxr_api->xr_result(result, "failed to get tracking for hand {0}!", i)) { - continue; - } - - // For some reason an inactive controller isn't coming back as inactive but has coordinates either as NAN or very large - const XrPosef &palm = hand_trackers[i].joint_locations[XR_HAND_JOINT_PALM_EXT].pose; - if ( - !hand_trackers[i].locations.isActive || isnan(palm.position.x) || palm.position.x < -1000000.00 || palm.position.x > 1000000.00) { - hand_trackers[i].locations.isActive = false; // workaround, make sure its inactive - } - - if (hand_tracking_aim_state_ext && hand_trackers[i].locations.isActive) { - // Controllers are updated based on the aim state's pose and pinches' strength - if (hand_trackers[i].aim_state_godot_controller == -1) { - hand_trackers[i].aim_state_godot_controller = - arvr_api->godot_arvr_add_controller( - const_cast(hand_controller_names[i]), - i + HAND_CONTROLLER_ID_OFFSET, - true, - true); - } - - hand_trackers[i].tracking_confidence = check_bit(XR_HAND_TRACKING_AIM_VALID_BIT_FB, hand_trackers[i].aimState.status) ? TrackingConfidence::TRACKING_CONFIDENCE_HIGH : TrackingConfidence::TRACKING_CONFIDENCE_NONE; - - int controller = hand_trackers[i].aim_state_godot_controller; - - const float ws = ARVRServer::get_singleton()->get_world_scale(); - godot_transform controller_transform; - auto *t = (Transform *)&controller_transform; - - *t = openxr_api->transform_from_pose(hand_trackers[i].aimState.aimPose, ws); - arvr_api->godot_arvr_set_controller_transform( - controller, - &controller_transform, - true, - true); - - // Index pinch is mapped to the A/X button - arvr_api->godot_arvr_set_controller_button( - controller, - 7, - check_bit(XR_HAND_TRACKING_AIM_INDEX_PINCHING_BIT_FB, hand_trackers[i].aimState.status)); - // Middle pinch is mapped to the B/Y button - arvr_api->godot_arvr_set_controller_button( - controller, - 1, - check_bit(XR_HAND_TRACKING_AIM_MIDDLE_PINCHING_BIT_FB, hand_trackers[i].aimState.status)); - // Ring pinch is mapped to the front trigger - arvr_api->godot_arvr_set_controller_button( - controller, - 15, - check_bit(XR_HAND_TRACKING_AIM_RING_PINCHING_BIT_FB, hand_trackers[i].aimState.status)); - // Little finger pinch is mapped to the side trigger / grip button - arvr_api->godot_arvr_set_controller_button( - controller, - 2, - check_bit(XR_HAND_TRACKING_AIM_LITTLE_PINCHING_BIT_FB, hand_trackers[i].aimState.status)); - // Menu button - arvr_api->godot_arvr_set_controller_button( - controller, - 3, - check_bit(XR_HAND_TRACKING_AIM_MENU_PRESSED_BIT_FB, hand_trackers[i].aimState.status)); - - // To allow accessing the pinch state as provided by the API we map them here to - // the joystick axis of the controller. This will give the ability to access the - // basic hand tracking gestures without the need to query specific APIs. - arvr_api->godot_arvr_set_controller_axis( - controller, - 7, - hand_trackers[i].aimState.pinchStrengthIndex, - false); - arvr_api->godot_arvr_set_controller_axis( - controller, - 6, - hand_trackers[i].aimState.pinchStrengthMiddle, - false); - arvr_api->godot_arvr_set_controller_axis( - controller, - 2, - hand_trackers[i].aimState.pinchStrengthRing, - false); - arvr_api->godot_arvr_set_controller_axis( - controller, - 4, - hand_trackers[i].aimState.pinchStrengthLittle, - false); - - } else if (hand_trackers[i].aim_state_godot_controller != -1) { - // Remove the controller, it's no longer active - arvr_api->godot_arvr_remove_controller(hand_trackers[i].aim_state_godot_controller); - hand_trackers[i].aim_state_godot_controller = -1; - hand_trackers[i].tracking_confidence = TrackingConfidence::TRACKING_CONFIDENCE_NONE; - } - } - } -} diff --git a/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.h b/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.h index a15f60c..c1ac9a0 100644 --- a/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.h +++ b/src/openxr/extensions/xr_ext_hand_tracking_extension_wrapper.h @@ -10,9 +10,15 @@ #define MAX_TRACKED_HANDS 2 #define HAND_CONTROLLER_ID_OFFSET 3 -class HandTracker { +class XRExtHandTrackingExtensionWrapper; + +class HandTracker : public OpenXRInputBase { +private: + XRExtHandTrackingExtensionWrapper *hand_tracking_api = nullptr; + public: bool is_initialised = false; + XrHandEXT hand_ext; XrHandJointsMotionRangeEXT motion_range = XR_HAND_JOINTS_MOTION_RANGE_UNOBSTRUCTED_EXT; XrHandTrackerEXT hand_tracker = XR_NULL_HANDLE; @@ -23,8 +29,10 @@ class HandTracker { XrHandJointVelocitiesEXT velocities; XrHandJointLocationsEXT locations; - godot_int aim_state_godot_controller = -1; - TrackingConfidence tracking_confidence = TrackingConfidence::TRACKING_CONFIDENCE_NONE; + HandTracker(XRExtHandTrackingExtensionWrapper *p_hand_tracking_api, const char *p_name, XrHandEXT p_hand_ext); + + virtual void update(OpenXRApi *p_openxr_api) override; + void cleanup(); }; // Wrapper for the XR hand tracking related extensions. @@ -37,16 +45,20 @@ class XRExtHandTrackingExtensionWrapper : public XRExtensionWrapper { static XRExtHandTrackingExtensionWrapper *get_singleton(); + virtual void add_input_maps() override; + void on_instance_initialized(const XrInstance instance) override; void on_state_ready() override; - void on_process_openxr() override; - void on_state_stopping() override; void on_session_destroyed() override; + bool is_supported() { return hand_tracking_supported; } + bool tracking_aim_state_supported() { return hand_tracking_aim_state_ext; } + bool hand_motion_supported() { return hand_motion_range_ext; } + const HandTracker *get_hand_tracker(uint32_t p_hand) const; XrHandJointsMotionRangeEXT get_motion_range(uint32_t p_hand) const; @@ -57,11 +69,6 @@ class XRExtHandTrackingExtensionWrapper : public XRExtensionWrapper { TrackingConfidence get_hand_tracker_tracking_confidence(const int p_godot_controller); -protected: - XRExtHandTrackingExtensionWrapper(); - ~XRExtHandTrackingExtensionWrapper(); - -private: static XRAPI_ATTR XrResult XRAPI_CALL xrCreateHandTrackerEXT( XrSession session, const XrHandTrackerCreateInfoEXT *createInfo, @@ -75,12 +82,15 @@ class XRExtHandTrackingExtensionWrapper : public XRExtensionWrapper { const XrHandJointsLocateInfoEXT *locateInfo, XrHandJointLocationsEXT *locations); +protected: + XRExtHandTrackingExtensionWrapper(); + ~XRExtHandTrackingExtensionWrapper(); + +private: static XrResult initialise_ext_hand_tracking_extension(XrInstance instance); bool initialize_hand_tracking(); - void update_handtracking(); - void cleanup_hand_tracking(); void cleanup(); @@ -93,7 +103,7 @@ class XRExtHandTrackingExtensionWrapper : public XRExtensionWrapper { bool hand_tracking_aim_state_ext = false; bool hand_tracking_supported = false; - HandTracker hand_trackers[MAX_TRACKED_HANDS]; // Fixed for left and right hand + HandTracker *hand_trackers[MAX_TRACKED_HANDS]; // Fixed for left and right hand }; #endif // XR_EXT_HAND_TRACKING_EXTENSION_WRAPPER_H diff --git a/src/openxr/extensions/xr_extension_wrapper.h b/src/openxr/extensions/xr_extension_wrapper.h index 9b8a95a..0604d45 100644 --- a/src/openxr/extensions/xr_extension_wrapper.h +++ b/src/openxr/extensions/xr_extension_wrapper.h @@ -25,6 +25,8 @@ class XRExtensionWrapper { virtual void on_session_initialized(const XrSession session) {} + virtual void add_input_maps() {} + virtual void on_state_idle() {} virtual void on_state_ready() {} diff --git a/src/openxr/extensions/xr_htc_vive_tracker_extension_wrapper.cpp b/src/openxr/extensions/xr_htc_vive_tracker_extension_wrapper.cpp new file mode 100644 index 0000000..baac5eb --- /dev/null +++ b/src/openxr/extensions/xr_htc_vive_tracker_extension_wrapper.cpp @@ -0,0 +1,139 @@ +#include + +#include "xr_htc_vive_tracker_extension_wrapper.h" + +XRHTCViveTrackerExtensionWrapper *XRHTCViveTrackerExtensionWrapper::singleton = nullptr; + +XRHTCViveTrackerExtensionWrapper *XRHTCViveTrackerExtensionWrapper::get_singleton() { + if (!singleton) { + singleton = new XRHTCViveTrackerExtensionWrapper(); + } + + return singleton; +} + +XRHTCViveTrackerExtensionWrapper::XRHTCViveTrackerExtensionWrapper() { + openxr_api = OpenXRApi::openxr_get_api(); + + request_extensions[XR_HTCX_VIVE_TRACKER_INTERACTION_EXTENSION_NAME] = &available; +} + +XRHTCViveTrackerExtensionWrapper::~XRHTCViveTrackerExtensionWrapper() { + OpenXRApi::openxr_release_api(); +} + +bool XRHTCViveTrackerExtensionWrapper::is_available() { + return available; +} + +void XRHTCViveTrackerExtensionWrapper::add_input_maps() { + if (available) { + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/left_foot")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/right_foot")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/left_shoulder")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/right_shoulder")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/left_elbow")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/right_elbow")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/left_knee")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/right_knee")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/waist")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/chest")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/camera")); + openxr_api->add_input_map(new OpenXRInputHTCTracker("/user/vive_tracker_htcx/role/keyboard")); + } +} + +bool XRHTCViveTrackerExtensionWrapper::on_event_polled(const XrEventDataBuffer &event) { + switch (event.type) { + case XR_TYPE_EVENT_DATA_VIVE_TRACKER_CONNECTED_HTCX: { + // Investigate if we need to do more here + printf("OpenXR EVENT: VIVE tracker connected"); + + return true; + } break; + default: { + return false; + } break; + } +} + +OpenXRInputHTCTracker::OpenXRInputHTCTracker(const char *p_name) : OpenXRInputBase(p_name, 0) { + // nothing to do here for now... +} + +void OpenXRInputHTCTracker::update(OpenXRApi *p_openxr_api) { + if (toplevel_path == XR_NULL_PATH) { + // no path, skip this + } else if (godot_controller == -1) { + // not Godot controller registered + } else { + const float ws = ARVRServer::get_singleton()->get_world_scale(); + OpenXRApi::DefaultAction *default_actions = &p_openxr_api->default_actions[0]; // Copy for easy access + bool is_active = false; + + // If our aim pose is active, our controller is active + // note, if the user has removed this action then our old controller approach becomes defunct + if (default_actions[OpenXRApi::ACTION_AIM_POSE].action != NULL) { + is_active = default_actions[OpenXRApi::ACTION_AIM_POSE].action->is_pose_active(toplevel_path); + } + + if (is_active) { + // Start with our pose, we put our ARVRController on our aim pose (may need to change this to our grip pose...) + godot_transform controller_transform; + Transform *t = (Transform *)&controller_transform; + tracking_confidence = default_actions[OpenXRApi::ACTION_AIM_POSE].action->get_as_pose(toplevel_path, ws, *t); + + if (tracking_confidence != TRACKING_CONFIDENCE_NONE) { + arvr_api->godot_arvr_set_controller_transform(godot_controller, &controller_transform, true, true); + } + + // For HTC trackers we have limited inputs + + // Button and axis are hardcoded.. + // Axis + if (default_actions[OpenXRApi::ACTION_FRONT_TRIGGER].action != NULL) { + arvr_api->godot_arvr_set_controller_axis(godot_controller, 2, default_actions[OpenXRApi::ACTION_FRONT_TRIGGER].action->get_as_float(toplevel_path), true); // 0.0 -> 1.0 + } + if (default_actions[OpenXRApi::ACTION_SIDE_TRIGGER].action != NULL) { + arvr_api->godot_arvr_set_controller_axis(godot_controller, 4, default_actions[OpenXRApi::ACTION_SIDE_TRIGGER].action->get_as_float(toplevel_path), true); // 0.0 -> 1.0 + } + if (default_actions[OpenXRApi::ACTION_PRIMARY].action != NULL) { + Vector2 v = default_actions[OpenXRApi::ACTION_PRIMARY].action->get_as_vector(toplevel_path); + arvr_api->godot_arvr_set_controller_axis(godot_controller, 0, v.x, true); // -1.0 -> 1.0 + arvr_api->godot_arvr_set_controller_axis(godot_controller, 1, v.y, true); // -1.0 -> 1.0 + } + + // Buttons + if (default_actions[OpenXRApi::ACTION_MENU_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 3, default_actions[OpenXRApi::ACTION_MENU_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_FRONT_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 15, default_actions[OpenXRApi::ACTION_FRONT_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_SIDE_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 2, default_actions[OpenXRApi::ACTION_SIDE_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_PRIMARY_BUTTON].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 14, default_actions[OpenXRApi::ACTION_PRIMARY_BUTTON].action->get_as_bool(toplevel_path)); + } + if (default_actions[OpenXRApi::ACTION_PRIMARY_TOUCH].action != NULL) { + arvr_api->godot_arvr_set_controller_button(godot_controller, 12, default_actions[OpenXRApi::ACTION_PRIMARY_TOUCH].action->get_as_bool(toplevel_path)); + } + + if (default_actions[OpenXRApi::ACTION_HAPTIC].action != NULL) { + // Godot currently only gives us a float between 0.0 and 1.0 for rumble strength. + // Full haptic control will be offered through another object + float haptic = arvr_api->godot_arvr_get_controller_rumble(godot_controller); + if (haptic > 0.0) { + // 17,000,000.0 nanoseconds (17ms) is slightly more then the duration of one frame if we're outputting at 60fps + // so if we sustain our pulse we should be issuing a new pulse before the old one ends + default_actions[OpenXRApi::ACTION_HAPTIC].action->do_haptic_pulse(toplevel_path, 17.0 * 1000 * 1000, XR_FREQUENCY_UNSPECIFIED, haptic); + } + } + } else { + // In Godot 4 this will also mark the tracker as inactive but in Godot 3 we'll need the user to check this manually + // But our old approach of removing and re-adding controllers means that IDs get messed up so... + tracking_confidence = TRACKING_CONFIDENCE_NONE; + } + } +} diff --git a/src/openxr/extensions/xr_htc_vive_tracker_extension_wrapper.h b/src/openxr/extensions/xr_htc_vive_tracker_extension_wrapper.h new file mode 100644 index 0000000..f6d0294 --- /dev/null +++ b/src/openxr/extensions/xr_htc_vive_tracker_extension_wrapper.h @@ -0,0 +1,33 @@ +#ifndef XR_HTC_VIVE_TRACKER_EXTENSION_H +#define XR_HTC_VIVE_TRACKER_EXTENSION_H + +#include "openxr/OpenXRApi.h" +#include "xr_extension_wrapper.h" +#include "openxr/include/util.h" + +class OpenXRInputHTCTracker : public OpenXRInputBase { +public: + OpenXRInputHTCTracker(const char *p_name); + + virtual void update(OpenXRApi *p_openxr_api) override; +}; + +class XRHTCViveTrackerExtensionWrapper : public XRExtensionWrapper { +public: + static XRHTCViveTrackerExtensionWrapper *get_singleton(); + + XRHTCViveTrackerExtensionWrapper(); + virtual ~XRHTCViveTrackerExtensionWrapper() override; + + bool is_available(); + virtual void add_input_maps() override; + virtual bool on_event_polled(const XrEventDataBuffer &event) override; + +private: + static XRHTCViveTrackerExtensionWrapper *singleton; + + OpenXRApi *openxr_api = nullptr; + bool available = false; +}; + +#endif // !XR_HTC_VIVE_TRACKER_EXTENSION_H