1585 lines · 68.2 KB · C++
RawDownload
//// 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.