Path: blob/master/modules/openxr/extensions/openxr_hand_tracking_extension.cpp
10278 views
/**************************************************************************/1/* openxr_hand_tracking_extension.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "openxr_hand_tracking_extension.h"3132#include "../openxr_api.h"3334#include "core/config/project_settings.h"35#include "core/string/print_string.h"36#include "servers/xr_server.h"3738#include <openxr/openxr.h>3940OpenXRHandTrackingExtension *OpenXRHandTrackingExtension::singleton = nullptr;4142OpenXRHandTrackingExtension *OpenXRHandTrackingExtension::get_singleton() {43return singleton;44}4546OpenXRHandTrackingExtension::OpenXRHandTrackingExtension() {47singleton = this;4849// Make sure this is cleared until we actually request it50handTrackingSystemProperties.supportsHandTracking = false;51}5253OpenXRHandTrackingExtension::~OpenXRHandTrackingExtension() {54singleton = nullptr;55}5657HashMap<String, bool *> OpenXRHandTrackingExtension::get_requested_extensions() {58HashMap<String, bool *> request_extensions;5960unobstructed_data_source = GLOBAL_GET("xr/openxr/extensions/hand_tracking_unobstructed_data_source");61controller_data_source = GLOBAL_GET("xr/openxr/extensions/hand_tracking_controller_data_source");6263request_extensions[XR_EXT_HAND_TRACKING_EXTENSION_NAME] = &hand_tracking_ext;64request_extensions[XR_EXT_HAND_JOINTS_MOTION_RANGE_EXTENSION_NAME] = &hand_motion_range_ext;65if (unobstructed_data_source || controller_data_source) {66request_extensions[XR_EXT_HAND_TRACKING_DATA_SOURCE_EXTENSION_NAME] = &hand_tracking_source_ext;67}6869return request_extensions;70}7172void OpenXRHandTrackingExtension::on_instance_created(const XrInstance p_instance) {73if (hand_tracking_ext) {74EXT_INIT_XR_FUNC(xrCreateHandTrackerEXT);75EXT_INIT_XR_FUNC(xrDestroyHandTrackerEXT);76EXT_INIT_XR_FUNC(xrLocateHandJointsEXT);7778hand_tracking_ext = xrCreateHandTrackerEXT_ptr && xrDestroyHandTrackerEXT_ptr && xrLocateHandJointsEXT_ptr;79}80}8182void OpenXRHandTrackingExtension::on_session_destroyed() {83cleanup_hand_tracking();84}8586void OpenXRHandTrackingExtension::on_instance_destroyed() {87xrCreateHandTrackerEXT_ptr = nullptr;88xrDestroyHandTrackerEXT_ptr = nullptr;89xrLocateHandJointsEXT_ptr = nullptr;90}9192void *OpenXRHandTrackingExtension::set_system_properties_and_get_next_pointer(void *p_next_pointer) {93if (!hand_tracking_ext) {94// not supported...95return p_next_pointer;96}9798handTrackingSystemProperties = {99XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT, // type100p_next_pointer, // next101false, // supportsHandTracking102};103104return &handTrackingSystemProperties;105}106107void OpenXRHandTrackingExtension::on_state_ready() {108if (!handTrackingSystemProperties.supportsHandTracking) {109// not supported...110return;111}112113// Setup our hands and reset data114for (int i = 0; i < OPENXR_MAX_TRACKED_HANDS; i++) {115// we'll do this later116hand_trackers[i].is_initialized = false;117hand_trackers[i].hand_tracker = XR_NULL_HANDLE;118119hand_trackers[i].locations.isActive = false;120121for (int j = 0; j < XR_HAND_JOINT_COUNT_EXT; j++) {122hand_trackers[i].joint_locations[j] = { 0, { { 0.0, 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 } }, 0.0 };123hand_trackers[i].joint_velocities[j] = { 0, { 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0 } };124}125}126}127128void OpenXRHandTrackingExtension::on_process() {129if (!handTrackingSystemProperties.supportsHandTracking) {130// not supported...131return;132}133134// process our hands135const XrTime time = OpenXRAPI::get_singleton()->get_predicted_display_time();136if (time == 0) {137// we don't have timing info yet, or we're skipping a frame...138return;139}140141XrResult result;142143for (int i = 0; i < OPENXR_MAX_TRACKED_HANDS; i++) {144if (hand_trackers[i].hand_tracker == XR_NULL_HANDLE) {145void *next_pointer = nullptr;146147// Originally not all XR runtimes supported hand tracking data sourced both from controllers and normal hand tracking.148// With this extension we can indicate we wish to accept input from either or both sources.149// This functionality is subject to the abilities of the XR runtime and requires the data source extension.150// Note: If the data source extension is not available, no guarantees can be made on what the XR runtime supports.151uint32_t data_source_count = 0;152XrHandTrackingDataSourceEXT data_sources[2];153if (unobstructed_data_source) {154data_sources[data_source_count++] = XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT;155}156if (controller_data_source) {157data_sources[data_source_count++] = XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT;158}159XrHandTrackingDataSourceInfoEXT data_source_info = { XR_TYPE_HAND_TRACKING_DATA_SOURCE_INFO_EXT, next_pointer, data_source_count, data_sources };160if (hand_tracking_source_ext) {161// If supported include this info162next_pointer = &data_source_info;163}164165XrHandTrackerCreateInfoEXT create_info = {166XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT, // type167next_pointer, // next168i == 0 ? XR_HAND_LEFT_EXT : XR_HAND_RIGHT_EXT, // hand169XR_HAND_JOINT_SET_DEFAULT_EXT, // handJointSet170};171172result = xrCreateHandTrackerEXT(OpenXRAPI::get_singleton()->get_session(), &create_info, &hand_trackers[i].hand_tracker);173if (XR_FAILED(result)) {174// not successful? then we do nothing.175print_line("OpenXR: Failed to obtain hand tracking information [", OpenXRAPI::get_singleton()->get_error_string(result), "]");176hand_trackers[i].is_initialized = false;177} else {178next_pointer = nullptr;179180hand_trackers[i].velocities.type = XR_TYPE_HAND_JOINT_VELOCITIES_EXT;181hand_trackers[i].velocities.next = next_pointer;182hand_trackers[i].velocities.jointCount = XR_HAND_JOINT_COUNT_EXT;183hand_trackers[i].velocities.jointVelocities = hand_trackers[i].joint_velocities;184next_pointer = &hand_trackers[i].velocities;185186if (hand_tracking_source_ext) {187hand_trackers[i].data_source.type = XR_TYPE_HAND_TRACKING_DATA_SOURCE_STATE_EXT;188hand_trackers[i].data_source.next = next_pointer;189hand_trackers[i].data_source.isActive = false;190hand_trackers[i].data_source.dataSource = XrHandTrackingDataSourceEXT(0);191next_pointer = &hand_trackers[i].data_source;192}193194// Needed for vendor hand tracking extensions implemented from GDExtension.195for (OpenXRExtensionWrapper *wrapper : OpenXRAPI::get_singleton()->get_registered_extension_wrappers()) {196void *np = wrapper->set_hand_joint_locations_and_get_next_pointer(i, next_pointer);197if (np != nullptr) {198next_pointer = np;199}200}201202hand_trackers[i].locations.type = XR_TYPE_HAND_JOINT_LOCATIONS_EXT;203hand_trackers[i].locations.next = next_pointer;204hand_trackers[i].locations.isActive = false;205hand_trackers[i].locations.jointCount = XR_HAND_JOINT_COUNT_EXT;206hand_trackers[i].locations.jointLocations = hand_trackers[i].joint_locations;207208Ref<XRHandTracker> godot_tracker;209godot_tracker.instantiate();210godot_tracker->set_tracker_hand(i == 0 ? XRPositionalTracker::TRACKER_HAND_LEFT : XRPositionalTracker::TRACKER_HAND_RIGHT);211godot_tracker->set_tracker_name(i == 0 ? "/user/hand_tracker/left" : "/user/hand_tracker/right");212XRServer::get_singleton()->add_tracker(godot_tracker);213hand_trackers[i].godot_tracker = godot_tracker;214215hand_trackers[i].is_initialized = true;216}217}218219if (hand_trackers[i].is_initialized) {220Ref<XRHandTracker> godot_tracker = hand_trackers[i].godot_tracker;221void *next_pointer = nullptr;222223XrHandJointsMotionRangeInfoEXT motion_range_info = { XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT, next_pointer, hand_trackers[i].motion_range };224if (hand_motion_range_ext) {225next_pointer = &motion_range_info;226}227228XrHandJointsLocateInfoEXT locateInfo = {229XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT, // type230next_pointer, // next231OpenXRAPI::get_singleton()->get_play_space(), // baseSpace232time, // time233};234235result = xrLocateHandJointsEXT(hand_trackers[i].hand_tracker, &locateInfo, &hand_trackers[i].locations);236if (XR_FAILED(result)) {237// not successful? then we do nothing.238print_line("OpenXR: Failed to get tracking for hand", i, "[", OpenXRAPI::get_singleton()->get_error_string(result), "]");239godot_tracker->set_hand_tracking_source(XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN);240godot_tracker->set_has_tracking_data(false);241godot_tracker->invalidate_pose("default");242continue;243}244245// For some reason an inactive controller isn't coming back as inactive but has coordinates either as NAN or very large246const XrPosef &palm = hand_trackers[i].joint_locations[XR_HAND_JOINT_PALM_EXT].pose;247if (!hand_trackers[i].locations.isActive || std::isnan(palm.position.x) || palm.position.x < -1000000.00 || palm.position.x > 1000000.00) {248hand_trackers[i].locations.isActive = false; // workaround, make sure its inactive249}250251if (hand_trackers[i].locations.isActive) {252// SKELETON_RIG_HUMANOID bone adjustment. This rotation performs:253// OpenXR Z+ -> Godot Humanoid Y- (Back along the bone)254// OpenXR Y+ -> Godot Humanoid Z- (Out the back of the hand)255const Quaternion bone_adjustment(0.0, -Math::SQRT12, Math::SQRT12, 0.0);256257for (int joint = 0; joint < XR_HAND_JOINT_COUNT_EXT; joint++) {258const XrHandJointLocationEXT &location = hand_trackers[i].joint_locations[joint];259const XrHandJointVelocityEXT &velocity = hand_trackers[i].joint_velocities[joint];260const XrPosef &pose = location.pose;261262Transform3D transform;263Vector3 linear_velocity;264Vector3 angular_velocity;265BitField<XRHandTracker::HandJointFlags> flags = {};266267if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) {268if (pose.orientation.x != 0 || pose.orientation.y != 0 || pose.orientation.z != 0 || pose.orientation.w != 0) {269flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_ORIENTATION_VALID);270transform.basis = Basis(Quaternion(pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w) * bone_adjustment);271}272}273if (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) {274flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_POSITION_VALID);275transform.origin = Vector3(pose.position.x, pose.position.y, pose.position.z);276}277if (location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT) {278flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_ORIENTATION_TRACKED);279}280if (location.locationFlags & XR_SPACE_LOCATION_POSITION_TRACKED_BIT) {281flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_POSITION_TRACKED);282}283if (location.locationFlags & XR_SPACE_VELOCITY_LINEAR_VALID_BIT) {284flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_LINEAR_VELOCITY_VALID);285linear_velocity = Vector3(velocity.linearVelocity.x, velocity.linearVelocity.y, velocity.linearVelocity.z);286godot_tracker->set_hand_joint_linear_velocity((XRHandTracker::HandJoint)joint, linear_velocity);287}288if (location.locationFlags & XR_SPACE_VELOCITY_ANGULAR_VALID_BIT) {289flags.set_flag(XRHandTracker::HAND_JOINT_FLAG_ANGULAR_VELOCITY_VALID);290angular_velocity = Vector3(velocity.angularVelocity.x, velocity.angularVelocity.y, velocity.angularVelocity.z);291godot_tracker->set_hand_joint_angular_velocity((XRHandTracker::HandJoint)joint, angular_velocity);292}293294godot_tracker->set_hand_joint_flags((XRHandTracker::HandJoint)joint, flags);295godot_tracker->set_hand_joint_transform((XRHandTracker::HandJoint)joint, transform);296godot_tracker->set_hand_joint_radius((XRHandTracker::HandJoint)joint, location.radius);297298if (joint == XR_HAND_JOINT_PALM_EXT) {299if (location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) {300XrHandTrackingDataSourceStateEXT &data_source = hand_trackers[i].data_source;301302XRHandTracker::HandTrackingSource source = XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN;303if (hand_tracking_source_ext) {304if (!data_source.isActive) {305source = XRHandTracker::HAND_TRACKING_SOURCE_NOT_TRACKED;306} else if (data_source.dataSource == XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT) {307source = XRHandTracker::HAND_TRACKING_SOURCE_UNOBSTRUCTED;308} else if (data_source.dataSource == XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT) {309source = XRHandTracker::HAND_TRACKING_SOURCE_CONTROLLER;310} else {311// Data source shouldn't be active, if new data sources are added to OpenXR we need to enable them.312WARN_PRINT_ONCE("Unknown active data source found!");313source = XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN;314}315}316godot_tracker->set_hand_tracking_source(source);317godot_tracker->set_has_tracking_data(true);318godot_tracker->set_pose("default", transform, linear_velocity, angular_velocity);319} else {320godot_tracker->set_hand_tracking_source(hand_tracking_source_ext ? XRHandTracker::HAND_TRACKING_SOURCE_NOT_TRACKED : XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN);321godot_tracker->set_has_tracking_data(false);322godot_tracker->invalidate_pose("default");323}324}325}326} else {327godot_tracker->set_hand_tracking_source(hand_tracking_source_ext ? XRHandTracker::HAND_TRACKING_SOURCE_NOT_TRACKED : XRHandTracker::HAND_TRACKING_SOURCE_UNKNOWN);328godot_tracker->set_has_tracking_data(false);329godot_tracker->invalidate_pose("default");330}331}332}333}334335void OpenXRHandTrackingExtension::on_state_stopping() {336// cleanup337cleanup_hand_tracking();338}339340void OpenXRHandTrackingExtension::cleanup_hand_tracking() {341XRServer *xr_server = XRServer::get_singleton();342ERR_FAIL_NULL(xr_server);343344for (int i = 0; i < OPENXR_MAX_TRACKED_HANDS; i++) {345if (hand_trackers[i].hand_tracker != XR_NULL_HANDLE) {346xrDestroyHandTrackerEXT(hand_trackers[i].hand_tracker);347348hand_trackers[i].is_initialized = false;349hand_trackers[i].hand_tracker = XR_NULL_HANDLE;350351XRServer::get_singleton()->remove_tracker(hand_trackers[i].godot_tracker);352}353}354}355356bool OpenXRHandTrackingExtension::get_active() {357return handTrackingSystemProperties.supportsHandTracking;358}359360const OpenXRHandTrackingExtension::HandTracker *OpenXRHandTrackingExtension::get_hand_tracker(HandTrackedHands p_hand) const {361ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, nullptr);362363return &hand_trackers[p_hand];364}365366XrHandJointsMotionRangeEXT OpenXRHandTrackingExtension::get_motion_range(HandTrackedHands p_hand) const {367ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, XR_HAND_JOINTS_MOTION_RANGE_MAX_ENUM_EXT);368369return hand_trackers[p_hand].motion_range;370}371372OpenXRHandTrackingExtension::HandTrackedSource OpenXRHandTrackingExtension::get_hand_tracking_source(HandTrackedHands p_hand) const {373ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, OPENXR_SOURCE_UNKNOWN);374375if (hand_tracking_source_ext) {376if (!hand_trackers[p_hand].data_source.isActive) {377return OPENXR_SOURCE_NOT_TRACKED;378} else if (hand_trackers[p_hand].data_source.dataSource == XR_HAND_TRACKING_DATA_SOURCE_UNOBSTRUCTED_EXT) {379return OPENXR_SOURCE_UNOBSTRUCTED;380} else if (hand_trackers[p_hand].data_source.dataSource == XR_HAND_TRACKING_DATA_SOURCE_CONTROLLER_EXT) {381return OPENXR_SOURCE_CONTROLLER;382} else {383// Data source shouldn't be active, if new data sources are added to OpenXR we need to enable them.384WARN_PRINT_ONCE("Unknown active data source found!");385return OPENXR_SOURCE_UNKNOWN;386}387}388389return OPENXR_SOURCE_UNKNOWN;390}391392void OpenXRHandTrackingExtension::set_motion_range(HandTrackedHands p_hand, XrHandJointsMotionRangeEXT p_motion_range) {393ERR_FAIL_UNSIGNED_INDEX(p_hand, OPENXR_MAX_TRACKED_HANDS);394hand_trackers[p_hand].motion_range = p_motion_range;395}396397XrSpaceLocationFlags OpenXRHandTrackingExtension::get_hand_joint_location_flags(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {398ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, XrSpaceLocationFlags(0));399ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, XrSpaceLocationFlags(0));400401if (!hand_trackers[p_hand].is_initialized) {402return XrSpaceLocationFlags(0);403}404405const XrHandJointLocationEXT &location = hand_trackers[p_hand].joint_locations[p_joint];406return location.locationFlags;407}408409Quaternion OpenXRHandTrackingExtension::get_hand_joint_rotation(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {410ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, Quaternion());411ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, Quaternion());412413if (!hand_trackers[p_hand].is_initialized) {414return Quaternion();415}416417const XrHandJointLocationEXT &location = hand_trackers[p_hand].joint_locations[p_joint];418return Quaternion(location.pose.orientation.x, location.pose.orientation.y, location.pose.orientation.z, location.pose.orientation.w);419}420421Vector3 OpenXRHandTrackingExtension::get_hand_joint_position(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {422ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, Vector3());423ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, Vector3());424425if (!hand_trackers[p_hand].is_initialized) {426return Vector3();427}428429const XrHandJointLocationEXT &location = hand_trackers[p_hand].joint_locations[p_joint];430return Vector3(location.pose.position.x, location.pose.position.y, location.pose.position.z);431}432433float OpenXRHandTrackingExtension::get_hand_joint_radius(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {434ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, 0.0);435ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, 0.0);436437if (!hand_trackers[p_hand].is_initialized) {438return 0.0;439}440441return hand_trackers[p_hand].joint_locations[p_joint].radius;442}443444XrSpaceVelocityFlags OpenXRHandTrackingExtension::get_hand_joint_velocity_flags(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {445ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, XrSpaceVelocityFlags(0));446ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, XrSpaceVelocityFlags(0));447448if (!hand_trackers[p_hand].is_initialized) {449return XrSpaceVelocityFlags(0);450}451452const XrHandJointVelocityEXT &velocity = hand_trackers[p_hand].joint_velocities[p_joint];453return velocity.velocityFlags;454}455456Vector3 OpenXRHandTrackingExtension::get_hand_joint_linear_velocity(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {457ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, Vector3());458ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, Vector3());459460if (!hand_trackers[p_hand].is_initialized) {461return Vector3();462}463464const XrHandJointVelocityEXT &velocity = hand_trackers[p_hand].joint_velocities[p_joint];465return Vector3(velocity.linearVelocity.x, velocity.linearVelocity.y, velocity.linearVelocity.z);466}467468Vector3 OpenXRHandTrackingExtension::get_hand_joint_angular_velocity(HandTrackedHands p_hand, XrHandJointEXT p_joint) const {469ERR_FAIL_UNSIGNED_INDEX_V(p_hand, OPENXR_MAX_TRACKED_HANDS, Vector3());470ERR_FAIL_UNSIGNED_INDEX_V(p_joint, XR_HAND_JOINT_COUNT_EXT, Vector3());471472if (!hand_trackers[p_hand].is_initialized) {473return Vector3();474}475476const XrHandJointVelocityEXT &velocity = hand_trackers[p_hand].joint_velocities[p_joint];477return Vector3(velocity.angularVelocity.x, velocity.angularVelocity.y, velocity.angularVelocity.z);478}479480481