//// Overte OpenXR Plugin//// Copyright 2024 Lubosz Sarnecki// Copyright 2024 Overte e.V.//// SPDX-License-Identifier: Apache-2.0// #include <glm/ext.hpp>#include <QJsonArray>#include <tuple> #include "OpenXrInputPlugin.h" #include "AvatarConstants.h"#include "PathUtils.h" #include "SettingHandle.h"#include "controllers/UserInputMapper.h" Q_DECLARE_LOGGING_CATEGORY(xr_input_cat)Q_LOGGING_CATEGORY(xr_input_cat, "openxr.input") enum ExtraButtonChannel { LEFT_HAS_PRIMARY = controller::StandardButtonChannel::NUM_STANDARD_BUTTONS, LEFT_HAS_TRACKPAD, LEFT_HAS_THUMBSTICK, LEFT_HAS_CAPACITIVE_TOUCH, LEFT_HAS_TRIGGER_CLICK, RIGHT_HAS_PRIMARY, RIGHT_HAS_TRACKPAD, RIGHT_HAS_THUMBSTICK, RIGHT_HAS_CAPACITIVE_TOUCH, RIGHT_HAS_TRIGGER_CLICK, LEFT_TRACKPAD_TOUCH, RIGHT_TRACKPAD_TOUCH, LT_TOUCH, LT_VIRTUAL_CLICK, RT_TOUCH, RT_VIRTUAL_CLICK,}; enum ExtraAxisChannel { LEFT_TRACKPAD_X = controller::StandardAxisChannel::NUM_STANDARD_AXES, LEFT_TRACKPAD_Y, LEFT_TRACKPAD_PRESS, RIGHT_TRACKPAD_X, RIGHT_TRACKPAD_Y, RIGHT_TRACKPAD_PRESS,}; static const std::unordered_map<controller::StandardPoseChannel, QString> poseChannelToString = { {controller::HIPS, "Hips"}, {controller::LEFT_LEG, "LeftLeg"}, {controller::LEFT_FOOT, "LeftFoot"}, {controller::RIGHT_LEG, "RightLeg"}, {controller::RIGHT_FOOT, "RightFoot"}, {controller::SPINE2, "Spine2"}, {controller::HEAD, "Head"}, {controller::RIGHT_ARM, "RightArm"}, {controller::RIGHT_HAND, "RightHand"}, {controller::LEFT_ARM, "LeftArm"}, {controller::LEFT_HAND, "LeftHand"},}; static const std::unordered_map<QString, controller::StandardPoseChannel> stringToPoseChannel = { {"Hips", controller::HIPS}, {"LeftLeg", controller::LEFT_LEG}, {"LeftFoot", controller::LEFT_FOOT}, {"RightLeg", controller::RIGHT_LEG}, {"RightFoot", controller::RIGHT_FOOT}, {"Spine2", controller::SPINE2}, {"Head", controller::HEAD}, {"RightArm", controller::RIGHT_ARM}, {"RightHand", controller::RIGHT_HAND}, {"LeftArm", controller::LEFT_ARM}, {"LeftHand", controller::LEFT_HAND},}; OpenXrInputPlugin::OpenXrInputPlugin(std::shared_ptr<OpenXrContext> c) { _context = c; _inputDevice = std::make_shared<InputDevice>(_context);} static const QString XR_CONFIGURATION_LAYOUT = QString("OpenXrConfiguration.qml"); static glm::mat4 defaultPoseOffset(const controller::InputCalibrationData& data, controller::StandardPoseChannel channel) { switch (channel) { case controller::SPINE2: return data.defaultSpine2; case controller::HIPS: return data.defaultHips; case controller::LEFT_FOOT: return data.defaultLeftFoot; case controller::RIGHT_FOOT: return data.defaultRightFoot; default: return glm::mat4(); }} // FIXME: This currently only works if you're// standing in the center of your playspace// and are directly facing "forward". The// stage space locating should probably be// local or floor-local instead.void OpenXrInputPlugin::guessXDevRoles(std::unordered_map<XrXDevIdMNDX, XDevTracker>& tracker_map) { std::vector<std::tuple<XrXDevIdMNDX, glm::vec3, controller::Pose>> tracker_list; for (auto [id, tracker] : tracker_map) { XrResult result = XR_SUCCESS; XrSpaceLocation stageSpace = { XR_TYPE_SPACE_LOCATION }; XrSpaceLocation localSpace = { XR_TYPE_SPACE_LOCATION }; XrSpaceLocation headSpace = { XR_TYPE_SPACE_LOCATION }; result = xrLocateSpace(tracker.space, _context->_stageSpace, _context->_lastPredictedDisplayTime.value(), &stageSpace); xrCheck(_context->_instance, result, "guessXDevRoles: tracker stage space fail"); result = xrLocateSpace(tracker.space, _context->_viewSpace, _context->_lastPredictedDisplayTime.value(), &localSpace); xrCheck(_context->_instance, result, "guessXDevRoles: tracker local space fail"); result = xrLocateSpace(_context->_viewSpace, _context->_stageSpace, _context->_lastPredictedDisplayTime.value(), &headSpace); xrCheck(_context->_instance, result, "guessXDevRoles: head space fail"); // the tracker's position, relative horizontally to the headset // and vertically to the floor, normalized by the headset height // so we can check relative height rather than absolute meters auto position = xrVecToGlm(localSpace.pose.position); position.y = stageSpace.pose.position.y / headSpace.pose.position.y; tracker_list.push_back({ id, position, controller::Pose(xrVecToGlm(stageSpace.pose.position), xrQuatToGlm(stageSpace.pose.orientation)), }); } for (auto& tracker : tracker_list) { using namespace controller; auto position = std::get<1>(tracker); auto id = std::get<0>(tracker); auto& state = tracker_map[id]; // TODO: our input system only really expects 7-point tracking, // (i.e. head, hands, chest, hips, and feet), but we can expand // it later to support more joints if there's demand for it if (position.y < 0.2f) { state.pose_channel = position.x < 0.0f ? LEFT_FOOT : RIGHT_FOOT; } if (position.y > 0.4f && position.y < 0.65f) { state.pose_channel = HIPS; } if (position.y > 0.65f && position.y < 0.9f) { state.pose_channel = SPINE2; } qCDebug(xr_input_cat) << id << ":" << position.y << (state.pose_channel.has_value() ? poseChannelToString.at(state.pose_channel.value()) : "None"); }} void OpenXrInputPlugin::calibrate() { qCDebug(xr_input_cat) << "OpenXrInputPlugin::calibrate"; if (_context->_MNDX_xdevSpaceSupported) { guessXDevRoles(_inputDevice->_xdev); } _inputDevice->_trackerCalibrations.clear(); _inputDevice->_wantsCalibrate = true;} bool OpenXrInputPlugin::uncalibrate() { qCDebug(xr_input_cat) << "OpenXrInputPlugin::uncalibrate"; for (auto [_, tracker] : _inputDevice->_xdev) { tracker.pose_channel = {}; } _inputDevice->_trackerCalibrations.clear(); _inputDevice->_wantsCalibrate = false; return true;} bool OpenXrInputPlugin::isSupported() const { return _context->_isValid && _context->_isSupported;} void OpenXrInputPlugin::setConfigurationSettings(const QJsonObject configurationSettings) { const auto& calibration = configurationSettings["tracker_calibration"].toObject(); _inputDevice->_hapticsEnabled = configurationSettings["enable_haptics"].toBool(true); _inputDevice->_handTrackingEnabled = configurationSettings["enable_hand_tracking"].toBool(true); _context->_userPresenceEnabled = configurationSettings["enable_user_presence"].toBool(true); // grr qt5 doesnt support destructured iterating like std::map foreach (const auto& key, calibration.keys()) { const auto& value = calibration.value(key).toObject(); // some kind of invalid channel name, don't read it if (!stringToPoseChannel.contains(key)) { continue; } // broken setting, don't read it if (!value.contains("translation") || !value.contains("rotation")) { continue; } auto translationArray = value.value("translation").toArray(); auto rotationArray = value.value("rotation").toArray(); auto channel = stringToPoseChannel.at(key); auto translation = vec3(translationArray[0].toDouble(), translationArray[1].toDouble(), translationArray[2].toDouble()); auto rotation = quat(rotationArray[0].toDouble(), rotationArray[1].toDouble(), rotationArray[2].toDouble(), rotationArray[3].toDouble()); _inputDevice->_trackerCalibrations[channel] = controller::Pose(translation, rotation); }} QJsonObject OpenXrInputPlugin::configurationSettings() { QJsonObject calibration; for (const auto& [channel, pose] : _inputDevice->_trackerCalibrations) { const auto& translation = pose.getTranslation(); const auto& rotation = pose.getRotation(); auto object = QJsonObject { {"translation", QJsonArray { translation.x, translation.y, translation.z }}, {"rotation", QJsonArray { rotation.x, rotation.y, rotation.z, rotation.w }}, }; calibration[poseChannelToString.at(channel)] = object; } QJsonObject configurationSettings; configurationSettings["tracker_calibration"] = calibration; configurationSettings["enable_haptics"] = _inputDevice->_hapticsEnabled; configurationSettings["enable_hand_tracking"] = _inputDevice->_handTrackingEnabled; configurationSettings["enable_user_presence"] = _context->_userPresenceEnabled; return configurationSettings;} QString OpenXrInputPlugin::configurationLayout() { return XR_CONFIGURATION_LAYOUT;} bool OpenXrInputPlugin::activate() { if (!_context->_isValid) { return false; } InputPlugin::activate(); loadSettings(); // register with UserInputMapper auto userInputMapper = DependencyManager::get<controller::UserInputMapper>(); userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; return true;} void OpenXrInputPlugin::deactivate() { InputPlugin::deactivate(); _inputDevice->_poseStateMap.clear(); // unregister with UserInputMapper auto userInputMapper = DependencyManager::get<controller::UserInputMapper>(); userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; saveSettings();} void OpenXrInputPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (_context->_shouldQuit || !_context->_isValid) { deactivate(); return; } auto userInputMapper = DependencyManager::get<controller::UserInputMapper>(); if (_registeredWithInputMapper && !_context->_isDisplayActive) { userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; _inputDevice->_poseStateMap.clear(); return; } else if (!_registeredWithInputMapper && _context->_isDisplayActive) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; } if (!_registeredWithInputMapper) { return; } userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); }); if (_inputDevice->_trackedControllers == 0 && _registeredWithInputMapper) { userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; _inputDevice->_poseStateMap.clear(); } if (!_registeredWithInputMapper && _inputDevice->_trackedControllers > 0) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; }} void OpenXrInputPlugin::loadSettings() { Settings settings; settings.beginGroup(getName()); _inputDevice->_hapticsEnabled = settings.value("hapticsEnabled", true).toBool(); _inputDevice->_handTrackingEnabled = settings.value("handTrackingEnabled", true).toBool(); _context->_userPresenceEnabled = settings.value("userPresenceEnabled", true).toBool(); settings.beginGroup("trackerCalibration"); for (const auto& [name, channel] : stringToPoseChannel) { if (!settings.contains(name)) { continue; } vec3 translation = {}; quat rotation = {}; settings.getVec3ValueIfValid("translation", translation); settings.getQuatValueIfValid("rotation", rotation); _inputDevice->_trackerCalibrations.insert({ channel, controller::Pose(translation, rotation), }); } settings.endGroup(); settings.endGroup();} void OpenXrInputPlugin::saveSettings() const { Settings settings; settings.beginGroup(getName()); settings.setValue("hapticsEnabled", _inputDevice->_hapticsEnabled); settings.setValue("handTrackingEnabled", _inputDevice->_handTrackingEnabled); settings.setValue("userPresenceEnabled", _context->_userPresenceEnabled); // TODO: Should we save device serial->role mappings? // HTCX_vive_tracker_interaction has preset roles and doesn't // expose serials at all, and MNDX_xdev_space only has serials // with no preset roles. Other skeletal body tracking extensions // don't use serials either. // This isn't really useful for MNDX_xdev_space yet since the // recalibration step is also what assigns the roles to a tracker. settings.beginGroup("trackerCalibration"); for (const auto& [channel, pose] : _inputDevice->_trackerCalibrations) { settings.beginGroup(poseChannelToString.at(channel)); settings.setVec3Value("translation", pose.translation); settings.setQuatValue("rotation", pose.rotation); settings.endGroup(); } settings.endGroup(); settings.endGroup();} OpenXrInputPlugin::InputDevice::InputDevice(std::shared_ptr<OpenXrContext> c) : controller::InputDevice("OpenXR") { _context = c; qCDebug(xr_input_cat) << "Hand tracking supported:" << _context->_handTrackingSupported;} void OpenXrInputPlugin::InputDevice::focusOutEvent() { _axisStateMap.clear(); _buttonPressedMap.clear();}; bool OpenXrInputPlugin::InputDevice::triggerHapticPulse(float strength, float duration, uint16_t index) { if (index > 2 || !_hapticsEnabled) { return false; } std::unique_lock<std::recursive_mutex> locker(_lock); using namespace std::chrono; nanoseconds durationNs = duration_cast<nanoseconds>(milliseconds(static_cast<int>(duration))); XrDuration xrDuration = durationNs.count(); auto path = (index == 0) ? "left_haptic" : "right_haptic"; // FIXME: sometimes something bugs out and hammers this, // and the controller vibrates really loudly until another // haptic pulse is triggered if (!_actions.at(path)->applyHaptic(xrDuration, 50, 0.5f * strength)) { qCCritical(xr_input_cat) << "Failed to apply haptic feedback!"; } return true;} bool OpenXrInputPlugin::Action::init(XrActionSet actionSet) { XrInstance instance = _context->_instance; XrActionCreateInfo info = { .type = XR_TYPE_ACTION_CREATE_INFO, .actionType = _type, .countSubactionPaths = HAND_COUNT, .subactionPaths = _context->_handPaths, }; strncpy(info.actionName, _id.c_str(), XR_MAX_ACTION_NAME_SIZE - 1); strncpy(info.localizedActionName, _friendlyName.c_str(), XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1); XrResult result = xrCreateAction(actionSet, &info, &_action); if (!xrCheck(instance, result, "Failed to create action")) return false; // Pose actions need spaces if (_type == XR_ACTION_TYPE_POSE_INPUT) { if (!createPoseSpaces()) { return false; } } return true;} std::vector<XrActionSuggestedBinding> OpenXrInputPlugin::Action::getBindings() { assert(_action != XR_NULL_HANDLE); std::vector<XrActionSuggestedBinding> bindings; for (uint32_t i = 0; i < HAND_COUNT; i++) { XrPath path; xrStringToPath(_context->_instance, _id.c_str(), &path); XrActionSuggestedBinding binding = { .action = _action, .binding = path }; bindings.push_back(binding); } return bindings;} XrActionStateFloat OpenXrInputPlugin::Action::getFloat() { XrActionStateFloat state = { .type = XR_TYPE_ACTION_STATE_FLOAT, }; XrActionStateGetInfo info = { .type = XR_TYPE_ACTION_STATE_GET_INFO, .action = _action, }; XrResult result = xrGetActionStateFloat(_context->_session, &info, &state); xrCheck(_context->_instance, result, "Failed to get float state!"); return state;} XrActionStateVector2f OpenXrInputPlugin::Action::getVector2f() { XrActionStateVector2f state = { .type = XR_TYPE_ACTION_STATE_VECTOR2F, }; XrActionStateGetInfo info = { .type = XR_TYPE_ACTION_STATE_GET_INFO, .action = _action, }; XrResult result = xrGetActionStateVector2f(_context->_session, &info, &state); xrCheck(_context->_instance, result, "Failed to get vector2 state!"); return state;} XrActionStateBoolean OpenXrInputPlugin::Action::getBool() { XrActionStateBoolean state = { .type = XR_TYPE_ACTION_STATE_BOOLEAN, }; XrActionStateGetInfo info = { .type = XR_TYPE_ACTION_STATE_GET_INFO, .action = _action, }; XrResult result = xrGetActionStateBoolean(_context->_session, &info, &state); xrCheck(_context->_instance, result, "Failed to get float state!"); return state;} XrSpaceLocation OpenXrInputPlugin::Action::getPose() { XrActionStatePose state = { .type = XR_TYPE_ACTION_STATE_POSE, }; XrActionStateGetInfo info = {Lines 1–500 of 1585. Load more
// // Overte OpenXR Plugin // // Copyright 2024 Lubosz Sarnecki // Copyright 2024 Overte e.V. // // SPDX-License-Identifier: Apache-2.0 // #include <glm/ext.hpp> #include <QJsonArray> #include <tuple> #include "OpenXrInputPlugin.h" #include "AvatarConstants.h" #include "PathUtils.h" #include "SettingHandle.h" #include "controllers/UserInputMapper.h" Q_DECLARE_LOGGING_CATEGORY(xr_input_cat) Q_LOGGING_CATEGORY(xr_input_cat, "openxr.input") enum ExtraButtonChannel { LEFT_HAS_PRIMARY = controller::StandardButtonChannel::NUM_STANDARD_BUTTONS, LEFT_HAS_TRACKPAD, LEFT_HAS_THUMBSTICK, LEFT_HAS_CAPACITIVE_TOUCH, LEFT_HAS_TRIGGER_CLICK, RIGHT_HAS_PRIMARY, RIGHT_HAS_TRACKPAD, RIGHT_HAS_THUMBSTICK, RIGHT_HAS_CAPACITIVE_TOUCH, RIGHT_HAS_TRIGGER_CLICK, LEFT_TRACKPAD_TOUCH, RIGHT_TRACKPAD_TOUCH, LT_TOUCH, LT_VIRTUAL_CLICK, RT_TOUCH, RT_VIRTUAL_CLICK, }; enum ExtraAxisChannel { LEFT_TRACKPAD_X = controller::StandardAxisChannel::NUM_STANDARD_AXES, LEFT_TRACKPAD_Y, LEFT_TRACKPAD_PRESS, RIGHT_TRACKPAD_X, RIGHT_TRACKPAD_Y, RIGHT_TRACKPAD_PRESS, }; static const std::unordered_map<controller::StandardPoseChannel, QString> poseChannelToString = { {controller::HIPS, "Hips"}, {controller::LEFT_LEG, "LeftLeg"}, {controller::LEFT_FOOT, "LeftFoot"}, {controller::RIGHT_LEG, "RightLeg"}, {controller::RIGHT_FOOT, "RightFoot"}, {controller::SPINE2, "Spine2"}, {controller::HEAD, "Head"}, {controller::RIGHT_ARM, "RightArm"}, {controller::RIGHT_HAND, "RightHand"}, {controller::LEFT_ARM, "LeftArm"}, {controller::LEFT_HAND, "LeftHand"}, }; static const std::unordered_map<QString, controller::StandardPoseChannel> stringToPoseChannel = { {"Hips", controller::HIPS}, {"LeftLeg", controller::LEFT_LEG}, {"LeftFoot", controller::LEFT_FOOT}, {"RightLeg", controller::RIGHT_LEG}, {"RightFoot", controller::RIGHT_FOOT}, {"Spine2", controller::SPINE2}, {"Head", controller::HEAD}, {"RightArm", controller::RIGHT_ARM}, {"RightHand", controller::RIGHT_HAND}, {"LeftArm", controller::LEFT_ARM}, {"LeftHand", controller::LEFT_HAND}, }; OpenXrInputPlugin::OpenXrInputPlugin(std::shared_ptr<OpenXrContext> c) { _context = c; _inputDevice = std::make_shared<InputDevice>(_context); } static const QString XR_CONFIGURATION_LAYOUT = QString("OpenXrConfiguration.qml"); static glm::mat4 defaultPoseOffset(const controller::InputCalibrationData& data, controller::StandardPoseChannel channel) { switch (channel) { case controller::SPINE2: return data.defaultSpine2; case controller::HIPS: return data.defaultHips; case controller::LEFT_FOOT: return data.defaultLeftFoot; case controller::RIGHT_FOOT: return data.defaultRightFoot; default: return glm::mat4(); } } // FIXME: This currently only works if you're // standing in the center of your playspace // and are directly facing "forward". The // stage space locating should probably be // local or floor-local instead. void OpenXrInputPlugin::guessXDevRoles(std::unordered_map<XrXDevIdMNDX, XDevTracker>& tracker_map) { std::vector<std::tuple<XrXDevIdMNDX, glm::vec3, controller::Pose>> tracker_list; for (auto [id, tracker] : tracker_map) { XrResult result = XR_SUCCESS; XrSpaceLocation stageSpace = { XR_TYPE_SPACE_LOCATION }; XrSpaceLocation localSpace = { XR_TYPE_SPACE_LOCATION }; XrSpaceLocation headSpace = { XR_TYPE_SPACE_LOCATION }; result = xrLocateSpace(tracker.space, _context->_stageSpace, _context->_lastPredictedDisplayTime.value(), &stageSpace); xrCheck(_context->_instance, result, "guessXDevRoles: tracker stage space fail"); result = xrLocateSpace(tracker.space, _context->_viewSpace, _context->_lastPredictedDisplayTime.value(), &localSpace); xrCheck(_context->_instance, result, "guessXDevRoles: tracker local space fail"); result = xrLocateSpace(_context->_viewSpace, _context->_stageSpace, _context->_lastPredictedDisplayTime.value(), &headSpace); xrCheck(_context->_instance, result, "guessXDevRoles: head space fail"); // the tracker's position, relative horizontally to the headset // and vertically to the floor, normalized by the headset height // so we can check relative height rather than absolute meters auto position = xrVecToGlm(localSpace.pose.position); position.y = stageSpace.pose.position.y / headSpace.pose.position.y; tracker_list.push_back({ id, position, controller::Pose(xrVecToGlm(stageSpace.pose.position), xrQuatToGlm(stageSpace.pose.orientation)), }); } for (auto& tracker : tracker_list) { using namespace controller; auto position = std::get<1>(tracker); auto id = std::get<0>(tracker); auto& state = tracker_map[id]; // TODO: our input system only really expects 7-point tracking, // (i.e. head, hands, chest, hips, and feet), but we can expand // it later to support more joints if there's demand for it if (position.y < 0.2f) { state.pose_channel = position.x < 0.0f ? LEFT_FOOT : RIGHT_FOOT; } if (position.y > 0.4f && position.y < 0.65f) { state.pose_channel = HIPS; } if (position.y > 0.65f && position.y < 0.9f) { state.pose_channel = SPINE2; } qCDebug(xr_input_cat) << id << ":" << position.y << (state.pose_channel.has_value() ? poseChannelToString.at(state.pose_channel.value()) : "None"); } } void OpenXrInputPlugin::calibrate() { qCDebug(xr_input_cat) << "OpenXrInputPlugin::calibrate"; if (_context->_MNDX_xdevSpaceSupported) { guessXDevRoles(_inputDevice->_xdev); } _inputDevice->_trackerCalibrations.clear(); _inputDevice->_wantsCalibrate = true; } bool OpenXrInputPlugin::uncalibrate() { qCDebug(xr_input_cat) << "OpenXrInputPlugin::uncalibrate"; for (auto [_, tracker] : _inputDevice->_xdev) { tracker.pose_channel = {}; } _inputDevice->_trackerCalibrations.clear(); _inputDevice->_wantsCalibrate = false; return true; } bool OpenXrInputPlugin::isSupported() const { return _context->_isValid && _context->_isSupported; } void OpenXrInputPlugin::setConfigurationSettings(const QJsonObject configurationSettings) { const auto& calibration = configurationSettings["tracker_calibration"].toObject(); _inputDevice->_hapticsEnabled = configurationSettings["enable_haptics"].toBool(true); _inputDevice->_handTrackingEnabled = configurationSettings["enable_hand_tracking"].toBool(true); _context->_userPresenceEnabled = configurationSettings["enable_user_presence"].toBool(true); // grr qt5 doesnt support destructured iterating like std::map foreach (const auto& key, calibration.keys()) { const auto& value = calibration.value(key).toObject(); // some kind of invalid channel name, don't read it if (!stringToPoseChannel.contains(key)) { continue; } // broken setting, don't read it if (!value.contains("translation") || !value.contains("rotation")) { continue; } auto translationArray = value.value("translation").toArray(); auto rotationArray = value.value("rotation").toArray(); auto channel = stringToPoseChannel.at(key); auto translation = vec3(translationArray[0].toDouble(), translationArray[1].toDouble(), translationArray[2].toDouble()); auto rotation = quat(rotationArray[0].toDouble(), rotationArray[1].toDouble(), rotationArray[2].toDouble(), rotationArray[3].toDouble()); _inputDevice->_trackerCalibrations[channel] = controller::Pose(translation, rotation); } } QJsonObject OpenXrInputPlugin::configurationSettings() { QJsonObject calibration; for (const auto& [channel, pose] : _inputDevice->_trackerCalibrations) { const auto& translation = pose.getTranslation(); const auto& rotation = pose.getRotation(); auto object = QJsonObject { {"translation", QJsonArray { translation.x, translation.y, translation.z }}, {"rotation", QJsonArray { rotation.x, rotation.y, rotation.z, rotation.w }}, }; calibration[poseChannelToString.at(channel)] = object; } QJsonObject configurationSettings; configurationSettings["tracker_calibration"] = calibration; configurationSettings["enable_haptics"] = _inputDevice->_hapticsEnabled; configurationSettings["enable_hand_tracking"] = _inputDevice->_handTrackingEnabled; configurationSettings["enable_user_presence"] = _context->_userPresenceEnabled; return configurationSettings; } QString OpenXrInputPlugin::configurationLayout() { return XR_CONFIGURATION_LAYOUT; } bool OpenXrInputPlugin::activate() { if (!_context->_isValid) { return false; } InputPlugin::activate(); loadSettings(); // register with UserInputMapper auto userInputMapper = DependencyManager::get<controller::UserInputMapper>(); userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; return true; } void OpenXrInputPlugin::deactivate() { InputPlugin::deactivate(); _inputDevice->_poseStateMap.clear(); // unregister with UserInputMapper auto userInputMapper = DependencyManager::get<controller::UserInputMapper>(); userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; saveSettings(); } void OpenXrInputPlugin::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { if (_context->_shouldQuit || !_context->_isValid) { deactivate(); return; } auto userInputMapper = DependencyManager::get<controller::UserInputMapper>(); if (_registeredWithInputMapper && !_context->_isDisplayActive) { userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; _inputDevice->_poseStateMap.clear(); return; } else if (!_registeredWithInputMapper && _context->_isDisplayActive) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; } if (!_registeredWithInputMapper) { return; } userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); }); if (_inputDevice->_trackedControllers == 0 && _registeredWithInputMapper) { userInputMapper->removeDevice(_inputDevice->_deviceID); _registeredWithInputMapper = false; _inputDevice->_poseStateMap.clear(); } if (!_registeredWithInputMapper && _inputDevice->_trackedControllers > 0) { userInputMapper->registerDevice(_inputDevice); _registeredWithInputMapper = true; } } void OpenXrInputPlugin::loadSettings() { Settings settings; settings.beginGroup(getName()); _inputDevice->_hapticsEnabled = settings.value("hapticsEnabled", true).toBool(); _inputDevice->_handTrackingEnabled = settings.value("handTrackingEnabled", true).toBool(); _context->_userPresenceEnabled = settings.value("userPresenceEnabled", true).toBool(); settings.beginGroup("trackerCalibration"); for (const auto& [name, channel] : stringToPoseChannel) { if (!settings.contains(name)) { continue; } vec3 translation = {}; quat rotation = {}; settings.getVec3ValueIfValid("translation", translation); settings.getQuatValueIfValid("rotation", rotation); _inputDevice->_trackerCalibrations.insert({ channel, controller::Pose(translation, rotation), }); } settings.endGroup(); settings.endGroup(); } void OpenXrInputPlugin::saveSettings() const { Settings settings; settings.beginGroup(getName()); settings.setValue("hapticsEnabled", _inputDevice->_hapticsEnabled); settings.setValue("handTrackingEnabled", _inputDevice->_handTrackingEnabled); settings.setValue("userPresenceEnabled", _context->_userPresenceEnabled); // TODO: Should we save device serial->role mappings? // HTCX_vive_tracker_interaction has preset roles and doesn't // expose serials at all, and MNDX_xdev_space only has serials // with no preset roles. Other skeletal body tracking extensions // don't use serials either. // This isn't really useful for MNDX_xdev_space yet since the // recalibration step is also what assigns the roles to a tracker. settings.beginGroup("trackerCalibration"); for (const auto& [channel, pose] : _inputDevice->_trackerCalibrations) { settings.beginGroup(poseChannelToString.at(channel)); settings.setVec3Value("translation", pose.translation); settings.setQuatValue("rotation", pose.rotation); settings.endGroup(); } settings.endGroup(); settings.endGroup(); } OpenXrInputPlugin::InputDevice::InputDevice(std::shared_ptr<OpenXrContext> c) : controller::InputDevice("OpenXR") { _context = c; qCDebug(xr_input_cat) << "Hand tracking supported:" << _context->_handTrackingSupported; } void OpenXrInputPlugin::InputDevice::focusOutEvent() { _axisStateMap.clear(); _buttonPressedMap.clear(); }; bool OpenXrInputPlugin::InputDevice::triggerHapticPulse(float strength, float duration, uint16_t index) { if (index > 2 || !_hapticsEnabled) { return false; } std::unique_lock<std::recursive_mutex> locker(_lock); using namespace std::chrono; nanoseconds durationNs = duration_cast<nanoseconds>(milliseconds(static_cast<int>(duration))); XrDuration xrDuration = durationNs.count(); auto path = (index == 0) ? "left_haptic" : "right_haptic"; // FIXME: sometimes something bugs out and hammers this, // and the controller vibrates really loudly until another // haptic pulse is triggered if (!_actions.at(path)->applyHaptic(xrDuration, 50, 0.5f * strength)) { qCCritical(xr_input_cat) << "Failed to apply haptic feedback!"; } return true; } bool OpenXrInputPlugin::Action::init(XrActionSet actionSet) { XrInstance instance = _context->_instance; XrActionCreateInfo info = { .type = XR_TYPE_ACTION_CREATE_INFO, .actionType = _type, .countSubactionPaths = HAND_COUNT, .subactionPaths = _context->_handPaths, }; strncpy(info.actionName, _id.c_str(), XR_MAX_ACTION_NAME_SIZE - 1); strncpy(info.localizedActionName, _friendlyName.c_str(), XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1); XrResult result = xrCreateAction(actionSet, &info, &_action); if (!xrCheck(instance, result, "Failed to create action")) return false; // Pose actions need spaces if (_type == XR_ACTION_TYPE_POSE_INPUT) { if (!createPoseSpaces()) { return false; } } return true; } std::vector<XrActionSuggestedBinding> OpenXrInputPlugin::Action::getBindings() { assert(_action != XR_NULL_HANDLE); std::vector<XrActionSuggestedBinding> bindings; for (uint32_t i = 0; i < HAND_COUNT; i++) { XrPath path; xrStringToPath(_context->_instance, _id.c_str(), &path); XrActionSuggestedBinding binding = { .action = _action, .binding = path }; bindings.push_back(binding); } return bindings; } XrActionStateFloat OpenXrInputPlugin::Action::getFloat() { XrActionStateFloat state = { .type = XR_TYPE_ACTION_STATE_FLOAT, }; XrActionStateGetInfo info = { .type = XR_TYPE_ACTION_STATE_GET_INFO, .action = _action, }; XrResult result = xrGetActionStateFloat(_context->_session, &info, &state); xrCheck(_context->_instance, result, "Failed to get float state!"); return state; } XrActionStateVector2f OpenXrInputPlugin::Action::getVector2f() { XrActionStateVector2f state = { .type = XR_TYPE_ACTION_STATE_VECTOR2F, }; XrActionStateGetInfo info = { .type = XR_TYPE_ACTION_STATE_GET_INFO, .action = _action, }; XrResult result = xrGetActionStateVector2f(_context->_session, &info, &state); xrCheck(_context->_instance, result, "Failed to get vector2 state!"); return state; } XrActionStateBoolean OpenXrInputPlugin::Action::getBool() { XrActionStateBoolean state = { .type = XR_TYPE_ACTION_STATE_BOOLEAN, }; XrActionStateGetInfo info = { .type = XR_TYPE_ACTION_STATE_GET_INFO, .action = _action, }; XrResult result = xrGetActionStateBoolean(_context->_session, &info, &state); xrCheck(_context->_instance, result, "Failed to get float state!"); return state; } XrSpaceLocation OpenXrInputPlugin::Action::getPose() { XrActionStatePose state = { .type = XR_TYPE_ACTION_STATE_POSE, }; XrActionStateGetInfo info = {