// // Created by Bradley Austin Davis on 2015/11/01 // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OpenVrHelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../interface/src/Menu.h" Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") using Mutex = std::mutex; using Lock = std::unique_lock; static int refCount { 0 }; static Mutex mutex; static vr::IVRSystem* activeHmd { nullptr }; static bool _openVrQuitRequested { false }; bool openVrQuitRequested() { return _openVrQuitRequested; } static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; bool isOculusPresent() { bool result = false; #if defined(Q_OS_WIN32) HANDLE oculusServiceEvent = ::OpenEventW(SYNCHRONIZE, FALSE, L"OculusHMDConnected"); // The existence of the service indicates a running Oculus runtime if (oculusServiceEvent) { // A signaled event indicates a connected HMD if (WAIT_OBJECT_0 == ::WaitForSingleObject(oculusServiceEvent, 0)) { result = true; } ::CloseHandle(oculusServiceEvent); } #endif return result; } bool oculusViaOpenVR() { static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); return enableDebugOpenVR && isOculusPresent() && vr::VR_IsHmdPresent(); } bool openVrSupported() { static const QString DEBUG_FLAG("HIFI_DEBUG_OPENVR"); static bool enableDebugOpenVR = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); return (enableDebugOpenVR || !isOculusPresent()) && vr::VR_IsHmdPresent(); } QString getVrSettingString(const char* section, const char* setting) { QString result; static const uint32_t BUFFER_SIZE = 1024; static char BUFFER[BUFFER_SIZE]; vr::IVRSettings * vrSettings = vr::VRSettings(); if (vrSettings) { vr::EVRSettingsError error = vr::VRSettingsError_None; vrSettings->GetString(vr::k_pch_audio_Section, vr::k_pch_audio_OnPlaybackDevice_String, BUFFER, BUFFER_SIZE, &error); if (error == vr::VRSettingsError_None) { result = BUFFER; } } return result; } vr::IVRSystem* acquireOpenVrSystem() { bool hmdPresent = vr::VR_IsHmdPresent(); if (hmdPresent) { Lock lock(mutex); if (!activeHmd) { #if DEV_BUILD qCDebug(displayplugins) << "OpenVR: No vr::IVRSystem instance active, building"; #endif vr::EVRInitError eError = vr::VRInitError_None; activeHmd = vr::VR_Init(&eError, vr::VRApplication_Scene); #if DEV_BUILD qCDebug(displayplugins) << "OpenVR display: HMD is " << activeHmd << " error is " << eError; #endif } if (activeHmd) { #if DEV_BUILD qCDebug(displayplugins) << "OpenVR: incrementing refcount"; #endif ++refCount; } } else { #if DEV_BUILD qCDebug(displayplugins) << "OpenVR: no hmd present"; #endif } return activeHmd; } void releaseOpenVrSystem() { if (activeHmd) { Lock lock(mutex); #if DEV_BUILD qCDebug(displayplugins) << "OpenVR: decrementing refcount"; #endif --refCount; if (0 == refCount) { #if DEV_BUILD qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; #endif // HACK: workaround openvr crash, call submit with an invalid texture, right before VR_Shutdown. const void* INVALID_GL_TEXTURE_HANDLE = (void*)(uintptr_t)-1; vr::Texture_t vrTexture{ (void*)INVALID_GL_TEXTURE_HANDLE, vr::TextureType_OpenGL, vr::ColorSpace_Auto }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_LEFT{ 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_RIGHT{ 0.5f, 0, 1, 1 }; auto compositor = vr::VRCompositor(); if (compositor) { compositor->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); compositor->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); } vr::VR_Shutdown(); _openVrQuitRequested = false; activeHmd = nullptr; } } } static char textArray[8192]; static QMetaObject::Connection _focusConnection, _focusTextConnection, _overlayMenuConnection; extern bool _openVrDisplayActive; static vr::IVROverlay* _overlay { nullptr }; static QObject* _keyboardFocusObject { nullptr }; static QString _existingText; static Qt::InputMethodHints _currentHints; extern PoseData _nextSimPoseData; static bool _keyboardShown { false }; static bool _overlayRevealed { false }; static const uint32_t SHOW_KEYBOARD_DELAY_MS = 400; void updateFromOpenVrKeyboardInput() { auto chars = _overlay->GetKeyboardText(textArray, 8192); auto newText = QString(QByteArray(textArray, chars)); _keyboardFocusObject->setProperty("text", newText); //// TODO modify the new text to match the possible input hints: //// ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly //// ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly //QInputMethodEvent event(_existingText, QList()); //event.setCommitString(newText, 0, _existingText.size()); //qApp->sendEvent(_keyboardFocusObject, &event); } void finishOpenVrKeyboardInput() { auto offscreenUi = DependencyManager::get(); updateFromOpenVrKeyboardInput(); // Simulate an enter press on the top level window to trigger the action if (0 == (_currentHints & Qt::ImhMultiLine)) { qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::KeyboardModifiers(), QString("\n"))); qApp->sendEvent(offscreenUi->getWindow(), &QKeyEvent(QEvent::KeyRelease, Qt::Key_Return, Qt::KeyboardModifiers())); } } static const QString DEBUG_FLAG("HIFI_DISABLE_STEAM_VR_KEYBOARD"); bool disableSteamVrKeyboard = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); void enableOpenVrKeyboard(PluginContainer* container) { if (disableSteamVrKeyboard) { return; } auto offscreenUi = DependencyManager::get(); _overlay = vr::VROverlay(); auto menu = container->getPrimaryMenu(); auto action = menu->getActionForOption(MenuOption::Overlays); // When the overlays are revealed, suppress the keyboard from appearing on text focus for a tenth of a second. _overlayMenuConnection = QObject::connect(action, &QAction::triggered, [action] { if (action->isChecked()) { _overlayRevealed = true; const int KEYBOARD_DELAY_MS = 100; QTimer::singleShot(KEYBOARD_DELAY_MS, [&] { _overlayRevealed = false; }); } }); } void disableOpenVrKeyboard() { if (disableSteamVrKeyboard) { return; } QObject::disconnect(_overlayMenuConnection); QObject::disconnect(_focusTextConnection); QObject::disconnect(_focusConnection); } bool isOpenVrKeyboardShown() { return _keyboardShown; } void handleOpenVrEvents() { if (!activeHmd) { return; } Lock lock(mutex); if (!activeHmd) { return; } vr::VREvent_t event; while (activeHmd->PollNextEvent(&event, sizeof(event))) { switch (event.eventType) { case vr::VREvent_Quit: _openVrQuitRequested = true; activeHmd->AcknowledgeQuit_Exiting(); break; case vr::VREvent_KeyboardCharInput: // Make the focused field match the keyboard results, inclusive of combining characters and such. updateFromOpenVrKeyboardInput(); break; case vr::VREvent_KeyboardDone: finishOpenVrKeyboardInput(); // FALL THROUGH case vr::VREvent_KeyboardClosed: _keyboardFocusObject = nullptr; _keyboardShown = false; DependencyManager::get()->unfocusWindows(); break; default: break; } #if DEV_BUILD qDebug() << "OpenVR: Event " << activeHmd->GetEventTypeNameFromEnum((vr::EVREventType)event.eventType) << "(" << event.eventType << ")"; #endif } } controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity) { // When the sensor-to-world rotation is identity the coordinate axes look like this: // // user // forward // -z // | // y| user // y o----x right // o-----x user // | up // | // z // // Rift // From ABOVE the hand canonical axes looks like this: // // | | | | y | | | | // | | | | | | | | | // | | | | | // |left | / x---- + \ |right| // | _/ z \_ | // | | | | // | | | | // // So when the user is in Rift space facing the -zAxis with hands outstretched and palms down // the rotation to align the Touch axes with those of the hands is: // // touchToHand = halfTurnAboutY * quaterTurnAboutX // Due to how the Touch controllers fit into the palm there is an offset that is different for each hand. // You can think of this offset as the inverse of the measured rotation when the hands are posed, such that // the combination (measurement * offset) is identity at this orientation. // // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) // // An approximate offset for the Touch can be obtained by inspection: // // Qoffset = glm::inverse(glm::angleAxis(sign * PI/2.0f, zAxis) * glm::angleAxis(PI/4.0f, xAxis)) // // So the full equation is: // // Q = combinedMeasurement * touchToHand // // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) // // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) static const glm::quat yFlip = glm::angleAxis(PI, Vectors::UNIT_Y); static const glm::quat quarterX = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_X); static const glm::quat touchToHand = yFlip * quarterX; static const glm::quat leftQuarterZ = glm::angleAxis(-PI_OVER_TWO, Vectors::UNIT_Z); static const glm::quat rightQuarterZ = glm::angleAxis(PI_OVER_TWO, Vectors::UNIT_Z); static const glm::quat eighthX = glm::angleAxis(PI / 4.0f, Vectors::UNIT_X); static const glm::quat leftRotationOffset = glm::inverse(leftQuarterZ * eighthX) * touchToHand; static const glm::quat rightRotationOffset = glm::inverse(rightQuarterZ * eighthX) * touchToHand; // this needs to match the leftBasePosition in tutorial/viveControllerConfiguration.js:21 static const float CONTROLLER_LATERAL_OFFSET = 0.0381f; static const float CONTROLLER_VERTICAL_OFFSET = 0.0495f; static const float CONTROLLER_FORWARD_OFFSET = 0.1371f; static const glm::vec3 CONTROLLER_OFFSET(CONTROLLER_LATERAL_OFFSET, CONTROLLER_VERTICAL_OFFSET, CONTROLLER_FORWARD_OFFSET); static const glm::vec3 leftTranslationOffset = glm::vec3(-1.0f, 1.0f, 1.0f) * CONTROLLER_OFFSET; static const glm::vec3 rightTranslationOffset = CONTROLLER_OFFSET; auto translationOffset = (isLeftHand ? leftTranslationOffset : rightTranslationOffset); auto rotationOffset = (isLeftHand ? leftRotationOffset : rightRotationOffset); glm::vec3 position = extractTranslation(mat); glm::quat rotation = glm::normalize(glm::quat_cast(mat)); position += rotation * translationOffset; rotation = rotation * rotationOffset; // transform into avatar frame auto result = controller::Pose(position, rotation); // handle change in velocity due to translationOffset result.velocity = linearVelocity + glm::cross(angularVelocity, position - extractTranslation(mat)); result.angularVelocity = angularVelocity; return result; } #define FAILED_MIN_SPEC_OVERLAY_NAME "FailedMinSpecOverlay" #define FAILED_MIN_SPEC_OVERLAY_FRIENDLY_NAME "Minimum specifications for SteamVR not met" #define FAILED_MIN_SPEC_UPDATE_INTERVAL_MS 10 #define FAILED_MIN_SPEC_AUTO_QUIT_INTERVAL_MS (MSECS_PER_SECOND * 30) #define MIN_CORES_SPEC 3 void showMinSpecWarning() { auto vrSystem = acquireOpenVrSystem(); auto vrOverlay = vr::VROverlay(); if (!vrOverlay) { qFatal("Unable to initialize SteamVR overlay manager"); } vr::VROverlayHandle_t minSpecFailedOverlay = 0; if (vr::VROverlayError_None != vrOverlay->CreateOverlay(FAILED_MIN_SPEC_OVERLAY_NAME, FAILED_MIN_SPEC_OVERLAY_FRIENDLY_NAME, &minSpecFailedOverlay)) { qFatal("Unable to create overlay"); } // Needed here for PathUtils QCoreApplication miniApp(__argc, __argv); vrSystem->ResetSeatedZeroPose(); QString imagePath = PathUtils::resourcesPath() + "/images/steam-min-spec-failed.png"; vrOverlay->SetOverlayFromFile(minSpecFailedOverlay, imagePath.toLocal8Bit().toStdString().c_str()); vrOverlay->SetHighQualityOverlay(minSpecFailedOverlay); vrOverlay->SetOverlayWidthInMeters(minSpecFailedOverlay, 1.4f); vrOverlay->SetOverlayInputMethod(minSpecFailedOverlay, vr::VROverlayInputMethod_Mouse); vrOverlay->ShowOverlay(minSpecFailedOverlay); QTimer* timer = new QTimer(&miniApp); timer->setInterval(FAILED_MIN_SPEC_UPDATE_INTERVAL_MS); // Qt::CoarseTimer acceptable, we don't need this to be frame rate accurate QObject::connect(timer, &QTimer::timeout, [&] { vr::TrackedDevicePose_t vrPoses[vr::k_unMaxTrackedDeviceCount]; vrSystem->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0, vrPoses, vr::k_unMaxTrackedDeviceCount); auto headPose = toGlm(vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); auto overlayPose = toOpenVr(headPose * glm::translate(glm::mat4(), vec3(0, 0, -1))); vrOverlay->SetOverlayTransformAbsolute(minSpecFailedOverlay, vr::TrackingUniverseSeated, &overlayPose); vr::VREvent_t event; while (vrSystem->PollNextEvent(&event, sizeof(event))) { switch (event.eventType) { case vr::VREvent_Quit: vrSystem->AcknowledgeQuit_Exiting(); QCoreApplication::quit(); break; case vr::VREvent_ButtonPress: // Quit on any button press except for 'putting on the headset' if (event.data.controller.button != vr::k_EButton_ProximitySensor) { QCoreApplication::quit(); } break; default: break; } } }); timer->start(); QTimer::singleShot(FAILED_MIN_SPEC_AUTO_QUIT_INTERVAL_MS, &miniApp, &QCoreApplication::quit); miniApp.exec(); } bool checkMinSpecImpl() { // If OpenVR isn't supported, we have no min spec, so pass if (!openVrSupported()) { return true; } // If we have at least MIN_CORES_SPEC cores, pass auto coreCount = QThread::idealThreadCount(); if (coreCount >= MIN_CORES_SPEC) { return true; } // Even if we have too few cores... if the compositor is using async reprojection, pass auto system = acquireOpenVrSystem(); auto compositor = vr::VRCompositor(); if (system && compositor) { vr::Compositor_FrameTiming timing; memset(&timing, 0, sizeof(timing)); timing.m_nSize = sizeof(vr::Compositor_FrameTiming); compositor->GetFrameTiming(&timing); releaseOpenVrSystem(); if (timing.m_nReprojectionFlags & VRCompositor_ReprojectionAsync) { return true; } } // We're using OpenVR and we don't have enough cores... showMinSpecWarning(); return false; } extern "C" { __declspec(dllexport) int __stdcall CheckMinSpec() { return checkMinSpecImpl() ? 1 : 0; } }