From 91de6392ca42486e7c3a9e8f12426269b9f7ecc8 Mon Sep 17 00:00:00 2001 From: Mads Buvik Sandvei Date: Fri, 26 Jun 2020 23:02:48 +0200 Subject: [PATCH] More refactoring / cleanup. Code policies. --- apps/openmw/CMakeLists.txt | 6 +- apps/openmw/engine.cpp | 2 +- apps/openmw/mwrender/animation.cpp | 4 - apps/openmw/mwvr/openxraction.cpp | 154 +-- apps/openmw/mwvr/openxraction.hpp | 1 + apps/openmw/mwvr/openxrinput.cpp | 535 +++++---- apps/openmw/mwvr/openxrinput.hpp | 93 +- apps/openmw/mwvr/openxrmanager.cpp | 19 +- apps/openmw/mwvr/openxrmanager.hpp | 4 +- apps/openmw/mwvr/openxrmanagerimpl.cpp | 7 +- apps/openmw/mwvr/openxrmanagerimpl.hpp | 123 +- apps/openmw/mwvr/openxrswapchain.hpp | 1 + apps/openmw/mwvr/openxrswapchainimpl.cpp | 87 +- apps/openmw/mwvr/openxrswapchainimpl.hpp | 66 +- apps/openmw/mwvr/realisticcombat.cpp | 632 +++++----- apps/openmw/mwvr/realisticcombat.hpp | 96 +- apps/openmw/mwvr/vranimation.cpp | 1197 +++++++++---------- apps/openmw/mwvr/vranimation.hpp | 129 +- apps/openmw/mwvr/vrenvironment.cpp | 12 +- apps/openmw/mwvr/vrenvironment.hpp | 8 +- apps/openmw/mwvr/vrgui.cpp | 1383 +++++++++++----------- apps/openmw/mwvr/vrgui.hpp | 8 +- apps/openmw/mwvr/vrinput.cpp | 238 ++-- apps/openmw/mwvr/vrinput.hpp | 323 ++--- apps/openmw/mwvr/vrinputmanager.cpp | 1114 ++++++++--------- apps/openmw/mwvr/vrinputmanager.hpp | 94 +- apps/openmw/mwvr/vrsession.cpp | 564 ++++----- apps/openmw/mwvr/vrsession.hpp | 149 +-- apps/openmw/mwvr/vrtexture.cpp | 15 +- apps/openmw/mwvr/vrtypes.cpp | 373 +++--- apps/openmw/mwvr/vrtypes.hpp | 178 ++- apps/openmw/mwvr/vrview.cpp | 21 +- apps/openmw/mwvr/vrview.hpp | 4 +- apps/openmw/mwvr/vrviewer.cpp | 135 +-- apps/openmw/mwvr/vrviewer.hpp | 13 +- cmake/FindOpenXR.cmake | 16 - 36 files changed, 3877 insertions(+), 3927 deletions(-) diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index a01f99c29..75e0ec201 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -120,6 +120,10 @@ else () endif () if(BUILD_VR_OPENXR) +# TODO: Move this into something akin to add_openmw_dir instead of breaking pattern. +# Later, openmw and openmw_vr should preferrably share game code as a static or shared library +# instead of being compiled separately, though for now that's not possible as i depend on +# USE_OPENXR preprocessor switches. set(OPENMW_VR_FILES vrengine.cpp mwvr/openxraction.hpp @@ -165,7 +169,7 @@ if(BUILD_VR_OPENXR) ${APPLE_BUNDLE_RESOURCES} ) - # Preprocessor variable used to control code paths between the vr port and non-vr. + # Preprocessor variable used to control code paths to vr code target_compile_options(openmw_vr PUBLIC -DUSE_OPENXR -DXR_USE_GRAPHICS_API_OPENGL -DXR_USE_PLATFORM_WIN32) endif() diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index bce9e1842..b041ecebc 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -555,7 +555,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) MWInput::InputManager* input = #ifdef USE_OPENXR - new MWVR::OpenXRInputManager(mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); + new MWVR::VRInputManager(mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); #else new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); #endif diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index a78967016..e4122a1a9 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1076,10 +1076,6 @@ namespace MWRender // TODO: It's difficult to design a good override system when // I don't have a good understanding of the animation code. So for // now i just hardcode blocking of updaters for nodes that should not be animated in VR. - - // TODO: Some overrides cause NaN during cull. - // I aassume this happens if an override causes a bone to never receive a valid matrix - // Add any bone+groupname pair that is messing with Vr comfort here. using Overrides = std::set; using GroupOverrides = std::map; diff --git a/apps/openmw/mwvr/openxraction.cpp b/apps/openmw/mwvr/openxraction.cpp index cc878a492..430bf83b2 100644 --- a/apps/openmw/mwvr/openxraction.cpp +++ b/apps/openmw/mwvr/openxraction.cpp @@ -5,83 +5,83 @@ namespace MWVR { -OpenXRAction::OpenXRAction( - XrAction action, - XrActionType actionType, - const std::string& actionName, - const std::string& localName) - : mAction(action) - , mType(actionType) - , mName(actionName) - , mLocalName(localName) -{ -}; - -OpenXRAction::~OpenXRAction() { - if (mAction) + OpenXRAction::OpenXRAction( + XrAction action, + XrActionType actionType, + const std::string& actionName, + const std::string& localName) + : mAction(action) + , mType(actionType) + , mName(actionName) + , mLocalName(localName) { - xrDestroyAction(mAction); + }; + + OpenXRAction::~OpenXRAction() { + if (mAction) + { + xrDestroyAction(mAction); + } + } + + bool OpenXRAction::getFloat(XrPath subactionPath, float& value) + { + auto* xr = Environment::get().getManager(); + XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO }; + getInfo.action = mAction; + getInfo.subactionPath = subactionPath; + + XrActionStateFloat xrValue{ XR_TYPE_ACTION_STATE_FLOAT }; + CHECK_XRCMD(xrGetActionStateFloat(xr->impl().xrSession(), &getInfo, &xrValue)); + + if (xrValue.isActive) + value = xrValue.currentState; + return xrValue.isActive; + } + + bool OpenXRAction::getBool(XrPath subactionPath, bool& value) + { + auto* xr = Environment::get().getManager(); + XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO }; + getInfo.action = mAction; + getInfo.subactionPath = subactionPath; + + XrActionStateBoolean xrValue{ XR_TYPE_ACTION_STATE_BOOLEAN }; + CHECK_XRCMD(xrGetActionStateBoolean(xr->impl().xrSession(), &getInfo, &xrValue)); + + if (xrValue.isActive) + value = xrValue.currentState; + return xrValue.isActive; + } + + // Pose action only checks if the pose is active or not + bool OpenXRAction::getPoseIsActive(XrPath subactionPath) + { + auto* xr = Environment::get().getManager(); + XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO }; + getInfo.action = mAction; + getInfo.subactionPath = subactionPath; + + XrActionStatePose xrValue{ XR_TYPE_ACTION_STATE_POSE }; + CHECK_XRCMD(xrGetActionStatePose(xr->impl().xrSession(), &getInfo, &xrValue)); + + return xrValue.isActive; + } + + bool OpenXRAction::applyHaptics(XrPath subactionPath, float amplitude) + { + amplitude = std::max(0.f, std::min(1.f, amplitude)); + + auto* xr = Environment::get().getManager(); + XrHapticVibration vibration{ XR_TYPE_HAPTIC_VIBRATION }; + vibration.amplitude = amplitude; + vibration.duration = XR_MIN_HAPTIC_DURATION; + vibration.frequency = XR_FREQUENCY_UNSPECIFIED; + + XrHapticActionInfo hapticActionInfo{ XR_TYPE_HAPTIC_ACTION_INFO }; + hapticActionInfo.action = mAction; + hapticActionInfo.subactionPath = subactionPath; + CHECK_XRCMD(xrApplyHapticFeedback(xr->impl().xrSession(), &hapticActionInfo, (XrHapticBaseHeader*)&vibration)); + return true; } } - -bool OpenXRAction::getFloat(XrPath subactionPath, float& value) -{ - auto* xr = Environment::get().getManager(); - XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO }; - getInfo.action = mAction; - getInfo.subactionPath = subactionPath; - - XrActionStateFloat xrValue{ XR_TYPE_ACTION_STATE_FLOAT }; - CHECK_XRCMD(xrGetActionStateFloat(xr->impl().xrSession(), &getInfo, &xrValue)); - - if (xrValue.isActive) - value = xrValue.currentState; - return xrValue.isActive; -} - -bool OpenXRAction::getBool(XrPath subactionPath, bool& value) -{ - auto* xr = Environment::get().getManager(); - XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO }; - getInfo.action = mAction; - getInfo.subactionPath = subactionPath; - - XrActionStateBoolean xrValue{ XR_TYPE_ACTION_STATE_BOOLEAN }; - CHECK_XRCMD(xrGetActionStateBoolean(xr->impl().xrSession(), &getInfo, &xrValue)); - - if (xrValue.isActive) - value = xrValue.currentState; - return xrValue.isActive; -} - -// Pose action only checks if the pose is active or not -bool OpenXRAction::getPoseIsActive(XrPath subactionPath) -{ - auto* xr = Environment::get().getManager(); - XrActionStateGetInfo getInfo{ XR_TYPE_ACTION_STATE_GET_INFO }; - getInfo.action = mAction; - getInfo.subactionPath = subactionPath; - - XrActionStatePose xrValue{ XR_TYPE_ACTION_STATE_POSE }; - CHECK_XRCMD(xrGetActionStatePose(xr->impl().xrSession(), &getInfo, &xrValue)); - - return xrValue.isActive; -} - -bool OpenXRAction::applyHaptics(XrPath subactionPath, float amplitude) -{ - amplitude = std::max(0.f, std::min(1.f, amplitude)); - - auto* xr = Environment::get().getManager(); - XrHapticVibration vibration{ XR_TYPE_HAPTIC_VIBRATION }; - vibration.amplitude = amplitude; - vibration.duration = XR_MIN_HAPTIC_DURATION; - vibration.frequency = XR_FREQUENCY_UNSPECIFIED; - - XrHapticActionInfo hapticActionInfo{ XR_TYPE_HAPTIC_ACTION_INFO }; - hapticActionInfo.action = mAction; - hapticActionInfo.subactionPath = subactionPath; - CHECK_XRCMD(xrApplyHapticFeedback(xr->impl().xrSession(), &hapticActionInfo, (XrHapticBaseHeader*)&vibration)); - return true; -} -} diff --git a/apps/openmw/mwvr/openxraction.hpp b/apps/openmw/mwvr/openxraction.hpp index 423df8c5d..dd2792049 100644 --- a/apps/openmw/mwvr/openxraction.hpp +++ b/apps/openmw/mwvr/openxraction.hpp @@ -8,6 +8,7 @@ namespace MWVR { + /// \brief C++ wrapper for the XrAction type struct OpenXRAction { private: diff --git a/apps/openmw/mwvr/openxrinput.cpp b/apps/openmw/mwvr/openxrinput.cpp index 9fcfa5305..9107a0f24 100644 --- a/apps/openmw/mwvr/openxrinput.cpp +++ b/apps/openmw/mwvr/openxrinput.cpp @@ -12,289 +12,288 @@ namespace MWVR { -OpenXRInput::OpenXRInput(const std::vector& suggestedBindings) - : mActionSet(createActionSet()) -{ - // When starting to account for more devices than oculus touch, this section may need some expansion/redesign. - - // Currently the set of action paths was determined using the oculus touch (i know nothing about the vive and the index). - // The set of action paths may therefore need expansion. E.g. /click vs /value may vary with controllers. - - // To fit more actions onto controllers i created a system of short and long press actions. Allowing one action to activate - // on a short press, and another on long. Here, what actions are short press and what actions are long press is simply - // hardcoded at init, rather than interpreted from bindings. That's bad, and should be fixed, but that's hard to do - // while staying true to openxr's binding system, so if the system i wrote for the oculus touch isn't a good fit for - // the vive/index, we might want to rewrite this to handle bindings ourselves. - generateControllerActionPaths(ActionPath::Select, "/input/select/click"); - generateControllerActionPaths(ActionPath::Squeeze, "/input/squeeze/value"); - generateControllerActionPaths(ActionPath::Pose, "/input/aim/pose"); - generateControllerActionPaths(ActionPath::Haptic, "/output/haptic"); - generateControllerActionPaths(ActionPath::Menu, "/input/menu/click"); - generateControllerActionPaths(ActionPath::ThumbstickX, "/input/thumbstick/x"); - generateControllerActionPaths(ActionPath::ThumbstickY, "/input/thumbstick/y"); - generateControllerActionPaths(ActionPath::ThumbstickClick, "/input/thumbstick/click"); - generateControllerActionPaths(ActionPath::X, "/input/x/click"); - generateControllerActionPaths(ActionPath::Y, "/input/y/click"); - generateControllerActionPaths(ActionPath::A, "/input/a/click"); - generateControllerActionPaths(ActionPath::B, "/input/b/click"); - generateControllerActionPaths(ActionPath::Trigger, "/input/trigger/value"); - - /* - // Applicable actions not (yet) included - A_QuickKey1, - A_QuickKey2, - A_QuickKey3, - A_QuickKey4, - A_QuickKey5, - A_QuickKey6, - A_QuickKey7, - A_QuickKey8, - A_QuickKey9, - A_QuickKey10, - A_QuickKeysMenu, - A_QuickLoad, - A_CycleSpellLeft, - A_CycleSpellRight, - A_CycleWeaponLeft, - A_CycleWeaponRight, - A_Screenshot, // Generate a VR screenshot? - A_Console, // Currently awkward due to a lack of virtual keyboard, but should be included when that's in place - */ - createMWAction(MWInput::A_GameMenu, "game_menu", "Game Menu"); - createMWAction(A_Recenter, "reposition_menu", "Reposition Menu"); - createMWAction(MWInput::A_Inventory, "inventory", "Inventory"); - createMWAction(MWInput::A_Activate, "activate", "Activate"); - createMWAction(MWInput::A_Use, "use", "Use"); - createMWAction(MWInput::A_Jump, "jump", "Jump"); - createMWAction(MWInput::A_ToggleWeapon, "weapon", "Weapon"); - createMWAction(MWInput::A_ToggleSpell, "spell", "Spell"); - createMWAction(MWInput::A_CycleSpellLeft, "cycle_spell_left", "Cycle Spell Left"); - createMWAction(MWInput::A_CycleSpellRight, "cycle_spell_right", "Cycle Spell Right"); - createMWAction(MWInput::A_CycleWeaponLeft, "cycle_weapon_left", "Cycle Weapon Left"); - createMWAction(MWInput::A_CycleWeaponRight, "cycle_weapon_right", "Cycle Weapon Right"); - createMWAction(MWInput::A_Sneak, "sneak", "Sneak"); - createMWAction(MWInput::A_QuickMenu, "quick_menu", "Quick Menu"); - createMWAction(MWInput::A_LookLeftRight, "look_left_right", "Look Left Right"); - createMWAction(MWInput::A_MoveForwardBackward, "move_forward_backward", "Move Forward Backward"); - createMWAction(MWInput::A_MoveLeftRight, "move_left_right", "Move Left Right"); - createMWAction(MWInput::A_Journal, "journal_book", "Journal Book"); - createMWAction(MWInput::A_QuickSave, "quick_save", "Quick Save"); - createMWAction(MWInput::A_Rest, "rest", "Rest"); - createMWAction(A_ActivateTouch, "activate_touched", "Activate Touch"); - createMWAction(MWInput::A_AlwaysRun, "always_run", "Always Run"); - createMWAction(MWInput::A_AutoMove, "auto_move", "Auto Move"); - createMWAction(MWInput::A_ToggleHUD, "toggle_hud", "Toggle HUD"); - createMWAction(MWInput::A_ToggleDebug, "toggle_debug", "Toggle DEBUG"); - createMWAction(A_MenuUpDown, "menu_up_down", "Menu Up Down"); - createMWAction(A_MenuLeftRight, "menu_left_right", "Menu Left Right"); - createMWAction(A_MenuSelect, "menu_select", "Menu Select"); - createMWAction(A_MenuBack, "menu_back", "Menu Back"); - createPoseAction(TrackedLimb::LEFT_HAND, "left_hand_pose", "Left Hand Pose"); - createPoseAction(TrackedLimb::RIGHT_HAND, "right_hand_pose", "Right Hand Pose"); - createHapticsAction(TrackedLimb::RIGHT_HAND, "right_hand_haptics", "Right Hand Haptics"); - createHapticsAction(TrackedLimb::LEFT_HAND, "left_hand_haptics", "Left Hand Haptics"); - - - for (auto& sb : suggestedBindings) - suggestBindings(sb); - - auto* xr = Environment::get().getManager(); - { // Set up the action set - XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO }; - attachInfo.countActionSets = 1; - attachInfo.actionSets = &mActionSet; - CHECK_XRCMD(xrAttachSessionActionSets(xr->impl().xrSession(), &attachInfo)); - } -}; - -void -OpenXRInput::createPoseAction( - TrackedLimb limb, - const std::string& actionName, - const std::string& localName) -{ - mTrackerMap.emplace(limb, new PoseAction(std::move(createXRAction(XR_ACTION_TYPE_POSE_INPUT, actionName, localName)))); -} - -void -OpenXRInput::createHapticsAction( - TrackedLimb limb, - const std::string& actionName, - const std::string& localName) -{ - mHapticsMap.emplace(limb, new HapticsAction(std::move(createXRAction(XR_ACTION_TYPE_VIBRATION_OUTPUT, actionName, localName)))); -} - -template -void -OpenXRInput::createMWAction( - int openMWAction, - const std::string& actionName, - const std::string& localName) -{ - mActionMap.emplace(openMWAction, new A(openMWAction, std::move(createXRAction(AT, actionName, localName)))); -} - -XrActionSet -OpenXRInput::createActionSet() -{ - auto* xr = Environment::get().getManager(); - XrActionSet actionSet = XR_NULL_HANDLE; - XrActionSetCreateInfo createInfo{ XR_TYPE_ACTION_SET_CREATE_INFO }; - strcpy_s(createInfo.actionSetName, "gameplay"); - strcpy_s(createInfo.localizedActionSetName, "Gameplay"); - createInfo.priority = 0; - CHECK_XRCMD(xrCreateActionSet(xr->impl().xrInstance(), &createInfo, &actionSet)); - return actionSet; -} - -void OpenXRInput::suggestBindings(const SuggestedBindings& mwSuggestedBindings) -{ - auto* xr = Environment::get().getManager(); - XrPath oculusTouchInteractionProfilePath; - CHECK_XRCMD( - xrStringToPath(xr->impl().xrInstance(), mwSuggestedBindings.controllerPath.c_str(), &oculusTouchInteractionProfilePath)); - - std::vector suggestedBindings = + OpenXRInput::OpenXRInput(const std::vector& suggestedBindings) + : mActionSet(createActionSet()) { - {*mTrackerMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Pose, Side::LEFT_SIDE)}, - {*mTrackerMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Pose, Side::RIGHT_SIDE)}, - {*mHapticsMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Haptic, Side::LEFT_SIDE)}, - {*mHapticsMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Haptic, Side::RIGHT_SIDE)}, + // When starting to account for more devices than oculus touch, this section may need some expansion/redesign. + + // Currently the set of action paths was determined using the oculus touch (i know nothing about the vive and the index). + // The set of action paths may therefore need expansion. E.g. /click vs /value may vary with controllers. + + // To fit more actions onto controllers i created a system of short and long press actions. Allowing one action to activate + // on a short press, and another on long. Here, what actions are short press and what actions are long press is simply + // hardcoded at init, rather than interpreted from bindings. That's bad, and should be fixed, but that's hard to do + // while staying true to openxr's binding system, so if the system i wrote for the oculus touch isn't a good fit for + // the vive/index, we might want to rewrite this to handle bindings ourselves. + generateControllerActionPaths(ActionPath::Select, "/input/select/click"); + generateControllerActionPaths(ActionPath::Squeeze, "/input/squeeze/value"); + generateControllerActionPaths(ActionPath::Pose, "/input/aim/pose"); + generateControllerActionPaths(ActionPath::Haptic, "/output/haptic"); + generateControllerActionPaths(ActionPath::Menu, "/input/menu/click"); + generateControllerActionPaths(ActionPath::ThumbstickX, "/input/thumbstick/x"); + generateControllerActionPaths(ActionPath::ThumbstickY, "/input/thumbstick/y"); + generateControllerActionPaths(ActionPath::ThumbstickClick, "/input/thumbstick/click"); + generateControllerActionPaths(ActionPath::X, "/input/x/click"); + generateControllerActionPaths(ActionPath::Y, "/input/y/click"); + generateControllerActionPaths(ActionPath::A, "/input/a/click"); + generateControllerActionPaths(ActionPath::B, "/input/b/click"); + generateControllerActionPaths(ActionPath::Trigger, "/input/trigger/value"); + + /* + // Applicable actions not (yet) included + A_QuickKey1, + A_QuickKey2, + A_QuickKey3, + A_QuickKey4, + A_QuickKey5, + A_QuickKey6, + A_QuickKey7, + A_QuickKey8, + A_QuickKey9, + A_QuickKey10, + A_QuickKeysMenu, + A_QuickLoad, + A_CycleSpellLeft, + A_CycleSpellRight, + A_CycleWeaponLeft, + A_CycleWeaponRight, + A_Screenshot, // Generate a VR screenshot? + A_Console, // Currently awkward due to a lack of virtual keyboard, but should be included when that's in place + */ + createMWAction(MWInput::A_GameMenu, "game_menu", "Game Menu"); + createMWAction(A_Recenter, "reposition_menu", "Reposition Menu"); + createMWAction(MWInput::A_Inventory, "inventory", "Inventory"); + createMWAction(MWInput::A_Activate, "activate", "Activate"); + createMWAction(MWInput::A_Use, "use", "Use"); + createMWAction(MWInput::A_Jump, "jump", "Jump"); + createMWAction(MWInput::A_ToggleWeapon, "weapon", "Weapon"); + createMWAction(MWInput::A_ToggleSpell, "spell", "Spell"); + createMWAction(MWInput::A_CycleSpellLeft, "cycle_spell_left", "Cycle Spell Left"); + createMWAction(MWInput::A_CycleSpellRight, "cycle_spell_right", "Cycle Spell Right"); + createMWAction(MWInput::A_CycleWeaponLeft, "cycle_weapon_left", "Cycle Weapon Left"); + createMWAction(MWInput::A_CycleWeaponRight, "cycle_weapon_right", "Cycle Weapon Right"); + createMWAction(MWInput::A_Sneak, "sneak", "Sneak"); + createMWAction(MWInput::A_QuickMenu, "quick_menu", "Quick Menu"); + createMWAction(MWInput::A_LookLeftRight, "look_left_right", "Look Left Right"); + createMWAction(MWInput::A_MoveForwardBackward, "move_forward_backward", "Move Forward Backward"); + createMWAction(MWInput::A_MoveLeftRight, "move_left_right", "Move Left Right"); + createMWAction(MWInput::A_Journal, "journal_book", "Journal Book"); + createMWAction(MWInput::A_QuickSave, "quick_save", "Quick Save"); + createMWAction(MWInput::A_Rest, "rest", "Rest"); + createMWAction(A_ActivateTouch, "activate_touched", "Activate Touch"); + createMWAction(MWInput::A_AlwaysRun, "always_run", "Always Run"); + createMWAction(MWInput::A_AutoMove, "auto_move", "Auto Move"); + createMWAction(MWInput::A_ToggleHUD, "toggle_hud", "Toggle HUD"); + createMWAction(MWInput::A_ToggleDebug, "toggle_debug", "Toggle DEBUG"); + createMWAction(A_MenuUpDown, "menu_up_down", "Menu Up Down"); + createMWAction(A_MenuLeftRight, "menu_left_right", "Menu Left Right"); + createMWAction(A_MenuSelect, "menu_select", "Menu Select"); + createMWAction(A_MenuBack, "menu_back", "Menu Back"); + createPoseAction(TrackedLimb::LEFT_HAND, "left_hand_pose", "Left Hand Pose"); + createPoseAction(TrackedLimb::RIGHT_HAND, "right_hand_pose", "Right Hand Pose"); + createHapticsAction(TrackedLimb::RIGHT_HAND, "right_hand_haptics", "Right Hand Haptics"); + createHapticsAction(TrackedLimb::LEFT_HAND, "left_hand_haptics", "Left Hand Haptics"); + + + for (auto& sb : suggestedBindings) + suggestBindings(sb); + + auto* xr = Environment::get().getManager(); + { // Set up the action set + XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO }; + attachInfo.countActionSets = 1; + attachInfo.actionSets = &mActionSet; + CHECK_XRCMD(xrAttachSessionActionSets(xr->impl().xrSession(), &attachInfo)); + } }; - for (auto& mwSuggestedBinding : mwSuggestedBindings.bindings) + void + OpenXRInput::createPoseAction( + TrackedLimb limb, + const std::string& actionName, + const std::string& localName) { - auto xrAction = mActionMap.find(mwSuggestedBinding.action); - if (xrAction == mActionMap.end()) + mTrackerMap.emplace(limb, new PoseAction(std::move(createXRAction(XR_ACTION_TYPE_POSE_INPUT, actionName, localName)))); + } + + void + OpenXRInput::createHapticsAction( + TrackedLimb limb, + const std::string& actionName, + const std::string& localName) + { + mHapticsMap.emplace(limb, new HapticsAction(std::move(createXRAction(XR_ACTION_TYPE_VIBRATION_OUTPUT, actionName, localName)))); + } + + template + void + OpenXRInput::createMWAction( + int openMWAction, + const std::string& actionName, + const std::string& localName) + { + mActionMap.emplace(openMWAction, new A(openMWAction, std::move(createXRAction(AT, actionName, localName)))); + } + + XrActionSet + OpenXRInput::createActionSet() + { + auto* xr = Environment::get().getManager(); + XrActionSet actionSet = XR_NULL_HANDLE; + XrActionSetCreateInfo createInfo{ XR_TYPE_ACTION_SET_CREATE_INFO }; + strcpy_s(createInfo.actionSetName, "gameplay"); + strcpy_s(createInfo.localizedActionSetName, "Gameplay"); + createInfo.priority = 0; + CHECK_XRCMD(xrCreateActionSet(xr->impl().xrInstance(), &createInfo, &actionSet)); + return actionSet; + } + + void OpenXRInput::suggestBindings(const SuggestedBindings& mwSuggestedBindings) + { + auto* xr = Environment::get().getManager(); + XrPath oculusTouchInteractionProfilePath; + CHECK_XRCMD( + xrStringToPath(xr->impl().xrInstance(), mwSuggestedBindings.controllerPath.c_str(), &oculusTouchInteractionProfilePath)); + + std::vector suggestedBindings = { - Log(Debug::Error) << "OpenXRInput: Unknown action " << mwSuggestedBinding.action; - continue; + {*mTrackerMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Pose, Side::LEFT_SIDE)}, + {*mTrackerMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Pose, Side::RIGHT_SIDE)}, + {*mHapticsMap[TrackedLimb::LEFT_HAND], getXrPath(ActionPath::Haptic, Side::LEFT_SIDE)}, + {*mHapticsMap[TrackedLimb::RIGHT_HAND], getXrPath(ActionPath::Haptic, Side::RIGHT_SIDE)}, + }; + + for (auto& mwSuggestedBinding : mwSuggestedBindings.bindings) + { + auto xrAction = mActionMap.find(mwSuggestedBinding.action); + if (xrAction == mActionMap.end()) + { + Log(Debug::Error) << "OpenXRInput: Unknown action " << mwSuggestedBinding.action; + continue; + } + suggestedBindings.push_back({ *xrAction->second, getXrPath(mwSuggestedBinding.path, mwSuggestedBinding.side) }); } - suggestedBindings.push_back({ *xrAction->second, getXrPath(mwSuggestedBinding.path, mwSuggestedBinding.side) }); + + XrInteractionProfileSuggestedBinding xrSuggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING }; + xrSuggestedBindings.interactionProfile = oculusTouchInteractionProfilePath; + xrSuggestedBindings.suggestedBindings = suggestedBindings.data(); + xrSuggestedBindings.countSuggestedBindings = (uint32_t)suggestedBindings.size(); + CHECK_XRCMD(xrSuggestInteractionProfileBindings(xr->impl().xrInstance(), &xrSuggestedBindings)); } - XrInteractionProfileSuggestedBinding xrSuggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING }; - xrSuggestedBindings.interactionProfile = oculusTouchInteractionProfilePath; - xrSuggestedBindings.suggestedBindings = suggestedBindings.data(); - xrSuggestedBindings.countSuggestedBindings = (uint32_t)suggestedBindings.size(); - CHECK_XRCMD(xrSuggestInteractionProfileBindings(xr->impl().xrInstance(), &xrSuggestedBindings)); -} - -void -OpenXRInput::generateControllerActionPaths( - ActionPath actionPath, - const std::string& controllerAction) -{ - auto* xr = Environment::get().getManager(); - ControllerActionPaths actionPaths; - - std::string left = std::string("/user/hand/left") + controllerAction; - std::string right = std::string("/user/hand/right") + controllerAction; - - CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), left.c_str(), &actionPaths[(int)Side::LEFT_SIDE])); - CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), right.c_str(), &actionPaths[(int)Side::RIGHT_SIDE])); - - mPathMap[actionPath] = actionPaths; -} - - -std::unique_ptr -OpenXRInput::createXRAction( - XrActionType actionType, - const std::string& actionName, - const std::string& localName) -{ - ActionPtr actionPtr = nullptr; - std::vector subactionPaths; - XrActionCreateInfo createInfo{ XR_TYPE_ACTION_CREATE_INFO }; - createInfo.actionType = actionType; - strcpy_s(createInfo.actionName, actionName.c_str()); - strcpy_s(createInfo.localizedActionName, localName.c_str()); - - XrAction action = XR_NULL_HANDLE; - CHECK_XRCMD(xrCreateAction(mActionSet, &createInfo, &action)); - return std::unique_ptr{new OpenXRAction{action, actionType, actionName, localName}}; -} - -void -OpenXRInput::updateControls() -{ - auto* xr = Environment::get().getManager(); - if (!xr->impl().xrSessionRunning()) - return; - - - const XrActiveActionSet activeActionSet{ mActionSet, XR_NULL_PATH }; - XrActionsSyncInfo syncInfo{ XR_TYPE_ACTIONS_SYNC_INFO }; - syncInfo.countActiveActionSets = 1; - syncInfo.activeActionSets = &activeActionSet; - CHECK_XRCMD(xrSyncActions(xr->impl().xrSession(), &syncInfo)); - - - // Note on update order: - // Actions are queued FIFO. - // For most actions this does not matter. - // However mMenuBack may end GuiMode. If it shares a key with a non-gui-mode action - // and were processed before that action, they would both be activated which is - // clearly not desired. - for (auto& action : mActionMap) - action.second->updateAndQueue(mActionQueue); -} - -XrPath OpenXRInput::generateXrPath(const std::string& path) -{ - auto* xr = Environment::get().getManager(); - XrPath xrpath = 0; - CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), path.c_str(), &xrpath)); - return xrpath; -} - -const Action* OpenXRInput::nextAction() -{ - if (mActionQueue.empty()) - return nullptr; - - const auto* action = mActionQueue.front(); - mActionQueue.pop_front(); - return action; - -} - -Pose -OpenXRInput::getLimbPose( - int64_t time, - TrackedLimb limb) -{ - auto it = mTrackerMap.find(limb); - if (it == mTrackerMap.end()) + void + OpenXRInput::generateControllerActionPaths( + ActionPath actionPath, + const std::string& controllerAction) { - Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb; - return Pose{}; + auto* xr = Environment::get().getManager(); + ControllerActionPaths actionPaths; + + std::string left = std::string("/user/hand/left") + controllerAction; + std::string right = std::string("/user/hand/right") + controllerAction; + + CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), left.c_str(), &actionPaths[(int)Side::LEFT_SIDE])); + CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), right.c_str(), &actionPaths[(int)Side::RIGHT_SIDE])); + + mPathMap[actionPath] = actionPaths; } - it->second->update(time); - return it->second->value(); -} -void OpenXRInput::applyHaptics(TrackedLimb limb, float intensity) -{ - auto it = mHapticsMap.find(limb); - if (it == mHapticsMap.end()) + std::unique_ptr + OpenXRInput::createXRAction( + XrActionType actionType, + const std::string& actionName, + const std::string& localName) { - Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb; - return; + std::vector subactionPaths; + XrActionCreateInfo createInfo{ XR_TYPE_ACTION_CREATE_INFO }; + createInfo.actionType = actionType; + strcpy_s(createInfo.actionName, actionName.c_str()); + strcpy_s(createInfo.localizedActionName, localName.c_str()); + + XrAction action = XR_NULL_HANDLE; + CHECK_XRCMD(xrCreateAction(mActionSet, &createInfo, &action)); + return std::unique_ptr{new OpenXRAction{ action, actionType, actionName, localName }}; } - it->second->apply(intensity); -} -XrPath OpenXRInput::getXrPath(ActionPath actionPath, Side side) -{ - auto it = mPathMap.find(actionPath); - if (it == mPathMap.end()) + void + OpenXRInput::updateControls() { - Log(Debug::Error) << "OpenXRInput: No such path: " << (int)actionPath; + auto* xr = Environment::get().getManager(); + if (!xr->impl().xrSessionRunning()) + return; + + + const XrActiveActionSet activeActionSet{ mActionSet, XR_NULL_PATH }; + XrActionsSyncInfo syncInfo{ XR_TYPE_ACTIONS_SYNC_INFO }; + syncInfo.countActiveActionSets = 1; + syncInfo.activeActionSets = &activeActionSet; + CHECK_XRCMD(xrSyncActions(xr->impl().xrSession(), &syncInfo)); + + + // Note on update order: + // Actions are queued FIFO. + // For most actions this does not matter. + // However mMenuBack may end GuiMode. If it shares a key with a non-gui-mode action + // and were processed before that action, they would both be activated which is + // clearly not desired. + for (auto& action : mActionMap) + action.second->updateAndQueue(mActionQueue); + } + + XrPath OpenXRInput::generateXrPath(const std::string& path) + { + auto* xr = Environment::get().getManager(); + XrPath xrpath = 0; + CHECK_XRCMD(xrStringToPath(xr->impl().xrInstance(), path.c_str(), &xrpath)); + return xrpath; + } + + const Action* OpenXRInput::nextAction() + { + if (mActionQueue.empty()) + return nullptr; + + const auto* action = mActionQueue.front(); + mActionQueue.pop_front(); + return action; + + } + + Pose + OpenXRInput::getLimbPose( + int64_t time, + TrackedLimb limb) + { + auto it = mTrackerMap.find(limb); + if (it == mTrackerMap.end()) + { + Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb; + return Pose{}; + } + + it->second->update(time); + return it->second->value(); + } + + void OpenXRInput::applyHaptics(TrackedLimb limb, float intensity) + { + auto it = mHapticsMap.find(limb); + if (it == mHapticsMap.end()) + { + Log(Debug::Error) << "OpenXRInput: No such tracker: " << limb; + return; + } + + it->second->apply(intensity); + } + XrPath OpenXRInput::getXrPath(ActionPath actionPath, Side side) + { + auto it = mPathMap.find(actionPath); + if (it == mPathMap.end()) + { + Log(Debug::Error) << "OpenXRInput: No such path: " << (int)actionPath; + } + return it->second[(int)side]; } - return it->second[(int)side]; -} } diff --git a/apps/openmw/mwvr/openxrinput.hpp b/apps/openmw/mwvr/openxrinput.hpp index ac17d8ec3..f8fe9ded3 100644 --- a/apps/openmw/mwvr/openxrinput.hpp +++ b/apps/openmw/mwvr/openxrinput.hpp @@ -8,61 +8,48 @@ namespace MWVR { - -struct SuggestedBindings -{ - struct Binding + /// \brief Generates and manages OpenXR Actions and ActionSets by generating openxr bindings from a list of SuggestedBindings structs. + class OpenXRInput { - int action; - ActionPath path; - Side side; + public: + using Actions = MWInput::Actions; + using ControllerActionPaths = std::array; + + OpenXRInput(const std::vector& suggestedBindings); + + //! Update all controls and queue any actions + void updateControls(); + + //! Get next action from queue (repeat until null is returned) + const Action* nextAction(); + + //! Get current pose of limb in space. + Pose getLimbPose(int64_t time, TrackedLimb limb); + + //! Apply haptics of the given intensity to the given limb + void applyHaptics(TrackedLimb limb, float intensity); + + protected: + template + void createMWAction(int openMWAction, const std::string& actionName, const std::string& localName); + void createPoseAction(TrackedLimb limb, const std::string& actionName, const std::string& localName); + void createHapticsAction(TrackedLimb limb, const std::string& actionName, const std::string& localName); + std::unique_ptr createXRAction(XrActionType actionType, const std::string& actionName, const std::string& localName); + XrPath generateXrPath(const std::string& path); + void generateControllerActionPaths(ActionPath actionPath, const std::string& controllerAction); + XrActionSet createActionSet(void); + void suggestBindings(const SuggestedBindings& suggestedBindings); + XrPath getXrPath(ActionPath actionPath, Side side); + + XrActionSet mActionSet{ nullptr }; + + std::map mPathMap; + std::map> mActionMap; + std::map> mTrackerMap; + std::map> mHapticsMap; + + std::deque mActionQueue{}; }; - - std::string controllerPath; - std::vector bindings; -}; - -class OpenXRInput -{ -public: - using Actions = MWInput::Actions; - using ControllerActionPaths = std::array; - - OpenXRInput(const std::vector& suggestedBindings); - - //! Update all controls and queue any actions - void updateControls(); - - //! Get next action from queue (repeat until null is returned) - const Action* nextAction(); - - //! Get current pose of limb in space. - Pose getLimbPose(int64_t time, TrackedLimb limb); - - //! Apply haptics of the given intensity to the given limb - void applyHaptics(TrackedLimb limb, float intensity); - -protected: - template - void createMWAction(int openMWAction, const std::string& actionName, const std::string& localName); - void createPoseAction(TrackedLimb limb, const std::string& actionName, const std::string& localName); - void createHapticsAction(TrackedLimb limb, const std::string& actionName, const std::string& localName); - std::unique_ptr createXRAction(XrActionType actionType, const std::string& actionName, const std::string& localName); - XrPath generateXrPath(const std::string& path); - void generateControllerActionPaths(ActionPath actionPath, const std::string& controllerAction); - XrActionSet createActionSet(void); - void suggestBindings(const SuggestedBindings& suggestedBindings); - XrPath getXrPath(ActionPath actionPath, Side side); - - XrActionSet mActionSet{ nullptr }; - - std::map mPathMap; - std::map> mActionMap; - std::map> mTrackerMap; - std::map> mHapticsMap; - - std::deque mActionQueue{}; -}; } #endif diff --git a/apps/openmw/mwvr/openxrmanager.cpp b/apps/openmw/mwvr/openxrmanager.cpp index bd98292db..d4a49b78a 100644 --- a/apps/openmw/mwvr/openxrmanager.cpp +++ b/apps/openmw/mwvr/openxrmanager.cpp @@ -1,24 +1,9 @@ -#include "vrenvironment.hpp" #include "openxrmanager.hpp" +#include "vrenvironment.hpp" #include "openxrmanagerimpl.hpp" #include "../mwinput/inputmanagerimp.hpp" #include -#include - -#include - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include namespace MWVR { @@ -82,7 +67,7 @@ namespace MWVR try { mPrivate = std::make_shared(); } - catch (std::exception & e) + catch (std::exception& e) { Log(Debug::Error) << "Exception thrown by OpenXR: " << e.what(); osg::ref_ptr state = gc->getState(); diff --git a/apps/openmw/mwvr/openxrmanager.hpp b/apps/openmw/mwvr/openxrmanager.hpp index ea6c954bb..2e2366902 100644 --- a/apps/openmw/mwvr/openxrmanager.hpp +++ b/apps/openmw/mwvr/openxrmanager.hpp @@ -20,16 +20,16 @@ struct XrCompositionLayerBaseHeader; namespace MWVR { - // Use the pimpl pattern to avoid cluttering the namespace with openxr dependencies. class OpenXRManagerImpl; + /// \brief Manage the openxr runtime and session class OpenXRManager : public osg::Referenced { public: class RealizeOperation : public osg::GraphicsOperation { public: - RealizeOperation() : osg::GraphicsOperation("OpenXRRealizeOperation", false){}; + RealizeOperation() : osg::GraphicsOperation("OpenXRRealizeOperation", false) {}; void operator()(osg::GraphicsContext* gc) override; virtual bool realized(); diff --git a/apps/openmw/mwvr/openxrmanagerimpl.cpp b/apps/openmw/mwvr/openxrmanagerimpl.cpp index 8f59f56ce..5c6df795e 100644 --- a/apps/openmw/mwvr/openxrmanagerimpl.cpp +++ b/apps/openmw/mwvr/openxrmanagerimpl.cpp @@ -1,4 +1,5 @@ #include "openxrmanagerimpl.hpp" + #include "openxrswapchain.hpp" #include "openxrswapchainimpl.hpp" #include "vrtexture.hpp" @@ -298,7 +299,7 @@ namespace MWVR xrLayer.pose = toXR(layer.pose); xrLayer.fov = toXR(layer.fov); xrLayer.next = nullptr; - + return xrLayer; } @@ -331,7 +332,7 @@ namespace MWVR wglMakeCurrent(DC, GLRC); } - std::array + std::array OpenXRManagerImpl::getPredictedViews( int64_t predictedDisplayTime, ReferenceSpace space) @@ -444,7 +445,7 @@ namespace MWVR switch (newState) { case XR_SESSION_STATE_READY: - //case XR_SESSION_STATE_IDLE: + //case XR_SESSION_STATE_IDLE: { XrSessionBeginInfo beginInfo{ XR_TYPE_SESSION_BEGIN_INFO }; beginInfo.primaryViewConfigurationType = mViewConfigType; diff --git a/apps/openmw/mwvr/openxrmanagerimpl.hpp b/apps/openmw/mwvr/openxrmanagerimpl.hpp index ccc364e6a..c8f26cada 100644 --- a/apps/openmw/mwvr/openxrmanagerimpl.hpp +++ b/apps/openmw/mwvr/openxrmanagerimpl.hpp @@ -25,79 +25,80 @@ namespace MWVR { -// Error management macros and functions. Should be used on every openxr call. + // Error management macros and functions. Should be used on every openxr call. #define CHK_STRINGIFY(x) #x #define TOSTRING(x) CHK_STRINGIFY(x) #define FILE_AND_LINE __FILE__ ":" TOSTRING(__LINE__) #define CHECK_XRCMD(cmd) CheckXrResult(cmd, #cmd, FILE_AND_LINE); #define CHECK_XRRESULT(res, cmdStr) CheckXrResult(res, cmdStr, FILE_AND_LINE); -XrResult CheckXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr); -std::string XrResultString(XrResult res); + XrResult CheckXrResult(XrResult res, const char* originator = nullptr, const char* sourceLocation = nullptr); + std::string XrResultString(XrResult res); -/// Conversion methods from openxr types to osg/mwvr types. Includes managing the differing conventions. -MWVR::Pose fromXR(XrPosef pose); -MWVR::FieldOfView fromXR(XrFovf fov); -osg::Vec3 fromXR(XrVector3f); -osg::Quat fromXR(XrQuaternionf quat); + /// Conversion methods from openxr types to osg/mwvr types. Includes managing the differing conventions. + MWVR::Pose fromXR(XrPosef pose); + MWVR::FieldOfView fromXR(XrFovf fov); + osg::Vec3 fromXR(XrVector3f); + osg::Quat fromXR(XrQuaternionf quat); -/// Conversion methods from osg/mwvr types to openxr types. Includes managing the differing conventions. -XrPosef toXR(MWVR::Pose pose); -XrFovf toXR(MWVR::FieldOfView fov); -XrVector3f toXR(osg::Vec3 v); -XrQuaternionf toXR(osg::Quat quat); + /// Conversion methods from osg/mwvr types to openxr types. Includes managing the differing conventions. + XrPosef toXR(MWVR::Pose pose); + XrFovf toXR(MWVR::FieldOfView fov); + XrVector3f toXR(osg::Vec3 v); + XrQuaternionf toXR(osg::Quat quat); -XrCompositionLayerProjectionView toXR(MWVR::CompositionLayerProjectionView layer); + XrCompositionLayerProjectionView toXR(MWVR::CompositionLayerProjectionView layer); -struct OpenXRManagerImpl -{ - OpenXRManagerImpl(void); - ~OpenXRManagerImpl(void); + /// \brief Implementation of OpenXRManager + struct OpenXRManagerImpl + { + OpenXRManagerImpl(void); + ~OpenXRManagerImpl(void); - void waitFrame(); - void beginFrame(); - void endFrame(int64_t displayTime, int layerCount, const std::array& layerStack); - bool xrSessionRunning() const { return mSessionRunning; } - std::array getPredictedViews(int64_t predictedDisplayTime, ReferenceSpace space); - MWVR::Pose getPredictedHeadPose(int64_t predictedDisplayTime, ReferenceSpace space); - void handleEvents(); - void enablePredictions(); - void disablePredictions(); - long long getLastPredictedDisplayTime(); - long long getLastPredictedDisplayPeriod(); - std::array getRecommendedSwapchainConfig() const; - XrSpace getReferenceSpace(ReferenceSpace space); - XrSession xrSession() const { return mSession; }; - XrInstance xrInstance() const { return mInstance; }; + void waitFrame(); + void beginFrame(); + void endFrame(int64_t displayTime, int layerCount, const std::array& layerStack); + bool xrSessionRunning() const { return mSessionRunning; } + std::array getPredictedViews(int64_t predictedDisplayTime, ReferenceSpace space); + MWVR::Pose getPredictedHeadPose(int64_t predictedDisplayTime, ReferenceSpace space); + void handleEvents(); + void enablePredictions(); + void disablePredictions(); + long long getLastPredictedDisplayTime(); + long long getLastPredictedDisplayPeriod(); + std::array getRecommendedSwapchainConfig() const; + XrSpace getReferenceSpace(ReferenceSpace space); + XrSession xrSession() const { return mSession; }; + XrInstance xrInstance() const { return mInstance; }; -protected: - void LogLayersAndExtensions(); - void LogInstanceInfo(); - void LogReferenceSpaces(); - const XrEventDataBaseHeader* nextEvent(); - void HandleSessionStateChanged(const XrEventDataSessionStateChanged& stateChangedEvent); + protected: + void LogLayersAndExtensions(); + void LogInstanceInfo(); + void LogReferenceSpaces(); + const XrEventDataBaseHeader* nextEvent(); + void HandleSessionStateChanged(const XrEventDataSessionStateChanged& stateChangedEvent); -private: - bool initialized = false; - bool mPredictionsEnabled = false; - XrInstance mInstance = XR_NULL_HANDLE; - XrSession mSession = XR_NULL_HANDLE; - XrSpace mSpace = XR_NULL_HANDLE; - XrFormFactor mFormFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; - XrViewConfigurationType mViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; - XrEnvironmentBlendMode mEnvironmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; - XrSystemId mSystemId = XR_NULL_SYSTEM_ID; - XrGraphicsBindingOpenGLWin32KHR mGraphicsBinding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR }; - XrSystemProperties mSystemProperties{ XR_TYPE_SYSTEM_PROPERTIES }; - std::array mConfigViews{ { {XR_TYPE_VIEW_CONFIGURATION_VIEW}, {XR_TYPE_VIEW_CONFIGURATION_VIEW} } }; - XrSpace mReferenceSpaceView = XR_NULL_HANDLE; - XrSpace mReferenceSpaceStage = XR_NULL_HANDLE; - XrEventDataBuffer mEventDataBuffer{ XR_TYPE_EVENT_DATA_BUFFER }; - XrFrameState mFrameState{}; - XrSessionState mSessionState = XR_SESSION_STATE_UNKNOWN; - bool mSessionRunning = false; - std::mutex mFrameStateMutex{}; - std::mutex mEventMutex{}; -}; + private: + bool initialized = false; + bool mPredictionsEnabled = false; + XrInstance mInstance = XR_NULL_HANDLE; + XrSession mSession = XR_NULL_HANDLE; + XrSpace mSpace = XR_NULL_HANDLE; + XrFormFactor mFormFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + XrViewConfigurationType mViewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + XrEnvironmentBlendMode mEnvironmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + XrSystemId mSystemId = XR_NULL_SYSTEM_ID; + XrGraphicsBindingOpenGLWin32KHR mGraphicsBinding{ XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR }; + XrSystemProperties mSystemProperties{ XR_TYPE_SYSTEM_PROPERTIES }; + std::array mConfigViews{ { {XR_TYPE_VIEW_CONFIGURATION_VIEW}, {XR_TYPE_VIEW_CONFIGURATION_VIEW} } }; + XrSpace mReferenceSpaceView = XR_NULL_HANDLE; + XrSpace mReferenceSpaceStage = XR_NULL_HANDLE; + XrEventDataBuffer mEventDataBuffer{ XR_TYPE_EVENT_DATA_BUFFER }; + XrFrameState mFrameState{}; + XrSessionState mSessionState = XR_SESSION_STATE_UNKNOWN; + bool mSessionRunning = false; + std::mutex mFrameStateMutex{}; + std::mutex mEventMutex{}; + }; } #endif diff --git a/apps/openmw/mwvr/openxrswapchain.hpp b/apps/openmw/mwvr/openxrswapchain.hpp index f5290a682..a15adb395 100644 --- a/apps/openmw/mwvr/openxrswapchain.hpp +++ b/apps/openmw/mwvr/openxrswapchain.hpp @@ -10,6 +10,7 @@ namespace MWVR { class OpenXRSwapchainImpl; + /// \brief Creation and management of openxr swapchains class OpenXRSwapchain { public: diff --git a/apps/openmw/mwvr/openxrswapchainimpl.cpp b/apps/openmw/mwvr/openxrswapchainimpl.cpp index 2703641ea..d73c3019f 100644 --- a/apps/openmw/mwvr/openxrswapchainimpl.cpp +++ b/apps/openmw/mwvr/openxrswapchainimpl.cpp @@ -11,8 +11,6 @@ #include namespace MWVR { - - OpenXRSwapchainImpl::OpenXRSwapchainImpl(osg::ref_ptr state, SwapchainConfig config) : mWidth((int)config.recommendedWidth) , mHeight((int)config.recommendedHeight) @@ -33,27 +31,45 @@ namespace MWVR { std::vector swapchainFormats(swapchainFormatCount); CHECK_XRCMD(xrEnumerateSwapchainFormats(xr->impl().xrSession(), (uint32_t)swapchainFormats.size(), &swapchainFormatCount, swapchainFormats.data())); - // List of supported color swapchain formats. - constexpr int64_t SupportedColorSwapchainFormats[] = { + // Find supported color swapchain format. + constexpr int64_t RequestedColorSwapchainFormats[] = { GL_RGBA8, GL_RGBA8_SNORM, }; auto swapchainFormatIt = - std::find_first_of(swapchainFormats.begin(), swapchainFormats.end(), std::begin(SupportedColorSwapchainFormats), - std::end(SupportedColorSwapchainFormats)); + std::find_first_of(swapchainFormats.begin(), swapchainFormats.end(), std::begin(RequestedColorSwapchainFormats), + std::end(RequestedColorSwapchainFormats)); if (swapchainFormatIt == swapchainFormats.end()) { - Log(Debug::Error) << "No swapchain format supported at runtime"; + throw std::runtime_error("Swapchain color format not supported"); } - mSwapchainColorFormat = *swapchainFormatIt; + // Find supported depth swapchain format. + constexpr int64_t RequestedDepthSwapchainFormats[] = { + GL_DEPTH_COMPONENT32F, + GL_DEPTH_COMPONENT24, + GL_DEPTH_COMPONENT16, + }; + + swapchainFormatIt = + std::find_first_of(swapchainFormats.begin(), swapchainFormats.end(), std::begin(RequestedDepthSwapchainFormats), + std::end(RequestedDepthSwapchainFormats)); + if (swapchainFormatIt == swapchainFormats.end()) { + throw std::runtime_error("Swapchain depth format not supported"); + } + mSwapchainDepthFormat = *swapchainFormatIt; + mSamples = Settings::Manager::getInt("antialiasing", "Video"); + // OpenXR requires a non-zero value + if (mSamples < 1) + mSamples = 1; + while (mSamples > 0) { Log(Debug::Verbose) << "Creating swapchain with dimensions Width=" << mWidth << " Heigh=" << mHeight << " SampleCount=" << mSamples; - // Create the swapchain. + // First create the swapchain of color buffers. XrSwapchainCreateInfo swapchainCreateInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO }; swapchainCreateInfo.arraySize = 1; swapchainCreateInfo.format = mSwapchainColorFormat; @@ -61,30 +77,38 @@ namespace MWVR { swapchainCreateInfo.height = mHeight; swapchainCreateInfo.mipCount = 1; swapchainCreateInfo.faceCount = 1; - swapchainCreateInfo.sampleCount = 1; + swapchainCreateInfo.sampleCount = mSamples; swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; - //CHECK_XRCMD(xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mSwapchain)); auto res = xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mSwapchain); - if (XR_SUCCEEDED(res)) - break; - else + if (!XR_SUCCEEDED(res)) { Log(Debug::Verbose) << "Failed to create swapchain with SampleCount=" << mSamples << ": " << XrResultString(res); mSamples /= 2; - if(mSamples == 0) - std::runtime_error(XrResultString(res)); + if (mSamples == 0) + throw std::runtime_error(XrResultString(res)); + continue; } + // Now create the swapchain of depth buffers. + swapchainCreateInfo.format = mSwapchainDepthFormat; + swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + res = xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mDepthSwapchain); + if (XR_SUCCEEDED(res)) + break; + else + throw std::runtime_error(XrResultString(res)); } uint32_t imageCount = 0; CHECK_XRCMD(xrEnumerateSwapchainImages(mSwapchain, 0, &imageCount, nullptr)); - mSwapchainImageBuffers.resize(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR }); CHECK_XRCMD(xrEnumerateSwapchainImages(mSwapchain, imageCount, &imageCount, reinterpret_cast(mSwapchainImageBuffers.data()))); - //for (const auto& swapchainImage : mSwapchainImageBuffers) - // mTextureBuffers.push_back(new VRTexture(state, swapchainImage.image, mWidth, mHeight, 0)); + + CHECK_XRCMD(xrEnumerateSwapchainImages(mDepthSwapchain, 0, &imageCount, nullptr)); + mDepthSwapchainImageBuffers.resize(imageCount, { XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR }); + CHECK_XRCMD(xrEnumerateSwapchainImages(mDepthSwapchain, imageCount, &imageCount, reinterpret_cast(mDepthSwapchainImageBuffers.data()))); + for (unsigned i = 0; i < imageCount; i++) - mRenderBuffers.emplace_back(new VRTexture(state, mWidth, mHeight, mSamples)); + mRenderBuffers.emplace_back(new VRTexture(state, mWidth, mHeight, mSamples, mSwapchainImageBuffers[i].image, mDepthSwapchainImageBuffers[i].image)); mSubImage.swapchain = mSwapchain; mSubImage.imageRect.offset = { 0, 0 }; @@ -99,12 +123,14 @@ namespace MWVR { VRTexture* OpenXRSwapchainImpl::renderBuffer() const { - return mRenderBuffers[mRenderBuffer].get(); + if (isAcquired()) + return mRenderBuffers[mAcquiredImageIndex].get(); + throw std::logic_error("Swapbuffer not acquired before use"); } uint32_t OpenXRSwapchainImpl::acquiredImage() const { - if(isAcquired()) + if (isAcquired()) return mSwapchainImageBuffers[mAcquiredImageIndex].image; throw std::logic_error("Swapbuffer not acquired before use"); } @@ -116,7 +142,7 @@ namespace MWVR { void OpenXRSwapchainImpl::beginFrame(osg::GraphicsContext* gc) { - mRenderBuffer = (mRenderBuffer + 1) % mRenderBuffers.size(); + acquire(gc); renderBuffer()->beginFrame(gc); } @@ -124,29 +150,34 @@ namespace MWVR { void OpenXRSwapchainImpl::endFrame(osg::GraphicsContext* gc) { - // Blit frame to swapchain - acquire(gc); - renderBuffer()->endFrame(gc, acquiredImage()); release(gc); } - void OpenXRSwapchainImpl::acquire(osg::GraphicsContext* gc) + void OpenXRSwapchainImpl::acquire(osg::GraphicsContext*) { XrSwapchainImageAcquireInfo acquireInfo{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; + // I am trusting that the openxr runtime won't diverge these indices so long as these are always called together. + // If some dumb ass implementation decides to violate this we'll just have to work around that if it actually happens. CHECK_XRCMD(xrAcquireSwapchainImage(mSwapchain, &acquireInfo, &mAcquiredImageIndex)); + uint32_t depthIndex = 0; + CHECK_XRCMD(xrAcquireSwapchainImage(mDepthSwapchain, &acquireInfo, &depthIndex)); + if (depthIndex != mAcquiredImageIndex) + Log(Debug::Warning) << "Depth and color indices diverged"; XrSwapchainImageWaitInfo waitInfo{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO }; waitInfo.timeout = XR_INFINITE_DURATION; CHECK_XRCMD(xrWaitSwapchainImage(mSwapchain, &waitInfo)); + CHECK_XRCMD(xrWaitSwapchainImage(mDepthSwapchain, &waitInfo)); mIsAcquired = true; } - void OpenXRSwapchainImpl::release(osg::GraphicsContext* gc) + void OpenXRSwapchainImpl::release(osg::GraphicsContext*) { mIsAcquired = false; XrSwapchainImageReleaseInfo releaseInfo{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; CHECK_XRCMD(xrReleaseSwapchainImage(mSwapchain, &releaseInfo)); + CHECK_XRCMD(xrReleaseSwapchainImage(mDepthSwapchain, &releaseInfo)); } } diff --git a/apps/openmw/mwvr/openxrswapchainimpl.hpp b/apps/openmw/mwvr/openxrswapchainimpl.hpp index 2f850cb59..14acac991 100644 --- a/apps/openmw/mwvr/openxrswapchainimpl.hpp +++ b/apps/openmw/mwvr/openxrswapchainimpl.hpp @@ -8,43 +8,45 @@ struct XrSwapchainSubImage; namespace MWVR { + /// \brief Implementation of OpenXRSwapchain + class OpenXRSwapchainImpl + { + public: + OpenXRSwapchainImpl(osg::ref_ptr state, SwapchainConfig config); + ~OpenXRSwapchainImpl(); -class OpenXRSwapchainImpl -{ -public: - OpenXRSwapchainImpl(osg::ref_ptr state, SwapchainConfig config); - ~OpenXRSwapchainImpl(); + void beginFrame(osg::GraphicsContext* gc); + void endFrame(osg::GraphicsContext* gc); + void acquire(osg::GraphicsContext* gc); + void release(osg::GraphicsContext* gc); - void beginFrame(osg::GraphicsContext* gc); - void endFrame(osg::GraphicsContext* gc); - void acquire(osg::GraphicsContext* gc); - void release(osg::GraphicsContext* gc); + VRTexture* renderBuffer() const; - VRTexture* renderBuffer() const; + uint32_t acquiredImage() const; - uint32_t acquiredImage() const; + bool isAcquired() const; + XrSwapchain xrSwapchain(void) const { return mSwapchain; }; + XrSwapchainSubImage xrSubImage(void) const { return mSubImage; }; + int width() const { return mWidth; }; + int height() const { return mHeight; }; + int samples() const { return mSamples; }; - bool isAcquired() const; - XrSwapchain xrSwapchain(void) const { return mSwapchain; }; - XrSwapchainSubImage xrSubImage(void) const { return mSubImage; }; - int width() const { return mWidth; }; - int height() const { return mHeight; }; - int samples() const { return mSamples; }; - - XrSwapchain mSwapchain = XR_NULL_HANDLE; - std::vector mSwapchainImageBuffers{}; - //std::vector > mTextureBuffers{}; - XrSwapchainSubImage mSubImage{}; - int32_t mWidth = -1; - int32_t mHeight = -1; - int32_t mSamples = -1; - int64_t mSwapchainColorFormat = -1; - uint32_t mFBO = 0; - std::vector > mRenderBuffers{}; - int mRenderBuffer{ 0 }; - uint32_t mAcquiredImageIndex{ 0 }; - bool mIsAcquired{ false }; -}; + XrSwapchain mSwapchain = XR_NULL_HANDLE; + XrSwapchain mDepthSwapchain = XR_NULL_HANDLE; + std::vector mSwapchainImageBuffers{}; + std::vector mDepthSwapchainImageBuffers{}; + XrSwapchainSubImage mSubImage{}; + int32_t mWidth = -1; + int32_t mHeight = -1; + int32_t mSamples = -1; + int64_t mSwapchainColorFormat = -1; + int64_t mSwapchainDepthFormat = -1; + uint32_t mFBO = 0; + std::vector > mRenderBuffers{}; + int mRenderBuffer{ 0 }; + uint32_t mAcquiredImageIndex{ 0 }; + bool mIsAcquired{ false }; + }; } #endif diff --git a/apps/openmw/mwvr/realisticcombat.cpp b/apps/openmw/mwvr/realisticcombat.cpp index 96e3306cf..54d4b2c11 100644 --- a/apps/openmw/mwvr/realisticcombat.cpp +++ b/apps/openmw/mwvr/realisticcombat.cpp @@ -33,324 +33,326 @@ #include -namespace MWVR { namespace RealisticCombat { +namespace MWVR { + namespace RealisticCombat { -static const char* stateToString(SwingState florida) -{ - switch (florida) - { - case SwingState_Cooldown: - return "Cooldown"; - case SwingState_Impact: - return "Impact"; - case SwingState_Ready: - return "Ready"; - case SwingState_Swing: - return "Swing"; - case SwingState_Launch: - return "Launch"; - } - return "Error, invalid enum"; -} -static const char* swingTypeToString(int type) -{ - switch (type) - { - case ESM::Weapon::AT_Chop: - return "Chop"; - case ESM::Weapon::AT_Slash: - return "Slash"; - case ESM::Weapon::AT_Thrust: - return "Thrust"; - case -1: - return "Fail"; - default: - return "Invalid"; - } -} - -StateMachine::StateMachine(MWWorld::Ptr ptr) : ptr(ptr) {} - -bool StateMachine::canSwing() -{ - if (swingType >= 0) - if (velocity >= minVelocity) - if (swingType != ESM::Weapon::AT_Thrust || thrustVelocity >= 0.f) - return true; - return false; -} - -// Actions common to all transitions -void StateMachine::transition( - SwingState newState) -{ - Log(Debug::Verbose) << "Transition:" << stateToString(state) << "->" << stateToString(newState); - maxSwingVelocity = 0.f; - timeSinceEnteredState = 0.f; - movementSinceEnteredState = 0.f; - state = newState; -} - -void StateMachine::reset() -{ - maxSwingVelocity = 0.f; - timeSinceEnteredState = 0.f; - velocity = 0.f; - previousPosition = osg::Vec3(0.f, 0.f, 0.f); - state = SwingState_Ready; -} - -static bool isMeleeWeapon(int type) -{ - if (MWMechanics::getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee) - return false; - if (type == ESM::Weapon::HandToHand) - return true; - if (type >= 0) - return true; - - return false; -} - -static bool isSideSwingValidForWeapon(int type) -{ - switch (type) - { - case ESM::Weapon::HandToHand: - case ESM::Weapon::BluntOneHand: - case ESM::Weapon::BluntTwoClose: - case ESM::Weapon::BluntTwoWide: - case ESM::Weapon::SpearTwoWide: - return true; - case ESM::Weapon::ShortBladeOneHand: - case ESM::Weapon::LongBladeOneHand: - case ESM::Weapon::LongBladeTwoHand: - case ESM::Weapon::AxeOneHand: - case ESM::Weapon::AxeTwoHand: - default: - return false; - } -} - -void StateMachine::update(float dt, bool enabled) -{ - auto* session = Environment::get().getSession(); - auto* world = MWBase::Environment::get().getWorld(); - auto& predictedPoses = session->predictedPoses(VRSession::FramePhase::Update); - auto& handPose = predictedPoses.hands[(int)MWVR::Side::RIGHT_SIDE]; - auto weaponType = world->getActiveWeaponType(); - - enabled = enabled && isMeleeWeapon(weaponType); - - if (mEnabled != enabled) - { - reset(); - mEnabled = enabled; - } - if (!enabled) - return; - - timeSinceEnteredState += dt; - - - // First determine direction of different swing types - - // Discover orientation of weapon - osg::Quat weaponDir = handPose.orientation; - - // Morrowind models do not hold weapons at a natural angle, so i rotate the hand forward - // to get a more natural angle on the weapon to allow more comfortable combat. - if (weaponType != ESM::Weapon::HandToHand) - weaponDir = osg::Quat(osg::PI_4, osg::Vec3{ 1,0,0 }) * weaponDir; - - // Thrust means stabbing in the direction of the weapon - osg::Vec3 thrustDirection = weaponDir * osg::Vec3{ 0,1,0 }; - - // Slash and Chop are vertical, relative to the orientation of the weapon (direction of the sharp edge / hammer) - osg::Vec3 slashChopDirection = weaponDir * osg::Vec3{ 0,0,1 }; - - // Side direction of the weapon (i.e. The blunt side of the sword) - osg::Vec3 sideDirection = weaponDir * osg::Vec3{ 1,0,0 }; - - - // Next determine current hand movement - - // If tracking is lost, openxr will return a position of 0 - // So i reset position when tracking is re-acquired to avoid a superspeed strike. - // Theoretically, the player's hand really could be at 0,0,0 - // but that's a super rare case so whatever. - if (previousPosition == osg::Vec3(0.f, 0.f, 0.f)) - previousPosition = handPose.position; - - osg::Vec3 movement = handPose.position - previousPosition; - movementSinceEnteredState += movement.length(); - previousPosition = handPose.position; - osg::Vec3 swingVector = movement / dt; - osg::Vec3 swingDirection = swingVector; - swingDirection.normalize(); - - // Compute swing velocities - - // Thrust follows the orientation of the weapon. Negative thrust = no attack. - thrustVelocity = swingVector * thrustDirection; - velocity = swingVector.length(); - - - if (isSideSwingValidForWeapon(weaponType)) - { - // Compute velocity in the plane normal to the thrust direction. - float thrustComponent = std::abs(thrustVelocity / velocity); - float planeComponent = std::sqrt(1 - thrustComponent * thrustComponent); - slashChopVelocity = velocity * planeComponent; - sideVelocity = -1000.f; - } - else - { - // If side swing is not valid for the weapon, count slash/chop only along in - // the direction of the weapon's edge. - slashChopVelocity = std::abs(swingVector * slashChopDirection); - sideVelocity = std::abs(swingVector * sideDirection); - } - - - float orientationVerticality = std::abs(thrustDirection * osg::Vec3{ 0,0,1 }); - float swingVerticality = std::abs(swingDirection * osg::Vec3{ 0,0,1 }); - - // Pick swing type based on greatest current velocity - // Note i use abs() of thrust velocity to prevent accidentally triggering - // chop/slash when player is withdrawing the weapon. - if (sideVelocity > std::abs(thrustVelocity) && sideVelocity > slashChopVelocity) - { - // Player is swinging with the "blunt" side of a weapon that - // cannot be used that way. - swingType = -1; - } - else if (std::abs(thrustVelocity) > slashChopVelocity) - { - swingType = ESM::Weapon::AT_Thrust; - } - else - { - // First check if the weapon is pointing upwards. In which case slash is not - // applicable, and the attack must be a chop. - if (orientationVerticality > 0.707) - swingType = ESM::Weapon::AT_Chop; - else + static const char* stateToString(SwingState florida) { - // Next check if the swing is more horizontal or vertical. A slash - // would be more horizontal. - if(swingVerticality > 0.707) - swingType = ESM::Weapon::AT_Chop; - else - swingType = ESM::Weapon::AT_Slash; + switch (florida) + { + case SwingState_Cooldown: + return "Cooldown"; + case SwingState_Impact: + return "Impact"; + case SwingState_Ready: + return "Ready"; + case SwingState_Swing: + return "Swing"; + case SwingState_Launch: + return "Launch"; + } + return "Error, invalid enum"; + } + static const char* swingTypeToString(int type) + { + switch (type) + { + case ESM::Weapon::AT_Chop: + return "Chop"; + case ESM::Weapon::AT_Slash: + return "Slash"; + case ESM::Weapon::AT_Thrust: + return "Thrust"; + case -1: + return "Fail"; + default: + return "Invalid"; + } + } + + StateMachine::StateMachine(MWWorld::Ptr ptr) : ptr(ptr) {} + + bool StateMachine::canSwing() + { + if (swingType >= 0) + if (velocity >= minVelocity) + if (swingType != ESM::Weapon::AT_Thrust || thrustVelocity >= 0.f) + return true; + return false; + } + + // Actions common to all transitions + void StateMachine::transition( + SwingState newState) + { + Log(Debug::Verbose) << "Transition:" << stateToString(state) << "->" << stateToString(newState); + maxSwingVelocity = 0.f; + timeSinceEnteredState = 0.f; + movementSinceEnteredState = 0.f; + state = newState; + } + + void StateMachine::reset() + { + maxSwingVelocity = 0.f; + timeSinceEnteredState = 0.f; + velocity = 0.f; + previousPosition = osg::Vec3(0.f, 0.f, 0.f); + state = SwingState_Ready; + } + + static bool isMeleeWeapon(int type) + { + if (MWMechanics::getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee) + return false; + if (type == ESM::Weapon::HandToHand) + return true; + if (type >= 0) + return true; + + return false; + } + + static bool isSideSwingValidForWeapon(int type) + { + switch (type) + { + case ESM::Weapon::HandToHand: + case ESM::Weapon::BluntOneHand: + case ESM::Weapon::BluntTwoClose: + case ESM::Weapon::BluntTwoWide: + case ESM::Weapon::SpearTwoWide: + return true; + case ESM::Weapon::ShortBladeOneHand: + case ESM::Weapon::LongBladeOneHand: + case ESM::Weapon::LongBladeTwoHand: + case ESM::Weapon::AxeOneHand: + case ESM::Weapon::AxeTwoHand: + default: + return false; + } + } + + void StateMachine::update(float dt, bool enabled) + { + auto* session = Environment::get().getSession(); + auto* world = MWBase::Environment::get().getWorld(); + auto& predictedPoses = session->predictedPoses(VRSession::FramePhase::Update); + auto& handPose = predictedPoses.hands[(int)MWVR::Side::RIGHT_SIDE]; + auto weaponType = world->getActiveWeaponType(); + + enabled = enabled && isMeleeWeapon(weaponType); + + if (mEnabled != enabled) + { + reset(); + mEnabled = enabled; + } + if (!enabled) + return; + + timeSinceEnteredState += dt; + + + // First determine direction of different swing types + + // Discover orientation of weapon + osg::Quat weaponDir = handPose.orientation; + + // Morrowind models do not hold weapons at a natural angle, so i rotate the hand forward + // to get a more natural angle on the weapon to allow more comfortable combat. + if (weaponType != ESM::Weapon::HandToHand) + weaponDir = osg::Quat(osg::PI_4, osg::Vec3{ 1,0,0 }) * weaponDir; + + // Thrust means stabbing in the direction of the weapon + osg::Vec3 thrustDirection = weaponDir * osg::Vec3{ 0,1,0 }; + + // Slash and Chop are vertical, relative to the orientation of the weapon (direction of the sharp edge / hammer) + osg::Vec3 slashChopDirection = weaponDir * osg::Vec3{ 0,0,1 }; + + // Side direction of the weapon (i.e. The blunt side of the sword) + osg::Vec3 sideDirection = weaponDir * osg::Vec3{ 1,0,0 }; + + + // Next determine current hand movement + + // If tracking is lost, openxr will return a position of 0 + // So i reset position when tracking is re-acquired to avoid a superspeed strike. + // Theoretically, the player's hand really could be at 0,0,0 + // but that's a super rare case so whatever. + if (previousPosition == osg::Vec3(0.f, 0.f, 0.f)) + previousPosition = handPose.position; + + osg::Vec3 movement = handPose.position - previousPosition; + movementSinceEnteredState += movement.length(); + previousPosition = handPose.position; + osg::Vec3 swingVector = movement / dt; + osg::Vec3 swingDirection = swingVector; + swingDirection.normalize(); + + // Compute swing velocities + + // Thrust follows the orientation of the weapon. Negative thrust = no attack. + thrustVelocity = swingVector * thrustDirection; + velocity = swingVector.length(); + + + if (isSideSwingValidForWeapon(weaponType)) + { + // Compute velocity in the plane normal to the thrust direction. + float thrustComponent = std::abs(thrustVelocity / velocity); + float planeComponent = std::sqrt(1 - thrustComponent * thrustComponent); + slashChopVelocity = velocity * planeComponent; + sideVelocity = -1000.f; + } + else + { + // If side swing is not valid for the weapon, count slash/chop only along in + // the direction of the weapon's edge. + slashChopVelocity = std::abs(swingVector * slashChopDirection); + sideVelocity = std::abs(swingVector * sideDirection); + } + + + float orientationVerticality = std::abs(thrustDirection * osg::Vec3{ 0,0,1 }); + float swingVerticality = std::abs(swingDirection * osg::Vec3{ 0,0,1 }); + + // Pick swing type based on greatest current velocity + // Note i use abs() of thrust velocity to prevent accidentally triggering + // chop/slash when player is withdrawing the weapon. + if (sideVelocity > std::abs(thrustVelocity) && sideVelocity > slashChopVelocity) + { + // Player is swinging with the "blunt" side of a weapon that + // cannot be used that way. + swingType = -1; + } + else if (std::abs(thrustVelocity) > slashChopVelocity) + { + swingType = ESM::Weapon::AT_Thrust; + } + else + { + // First check if the weapon is pointing upwards. In which case slash is not + // applicable, and the attack must be a chop. + if (orientationVerticality > 0.707) + swingType = ESM::Weapon::AT_Chop; + else + { + // Next check if the swing is more horizontal or vertical. A slash + // would be more horizontal. + if (swingVerticality > 0.707) + swingType = ESM::Weapon::AT_Chop; + else + swingType = ESM::Weapon::AT_Slash; + } + } + + switch (state) + { + case SwingState_Cooldown: + return update_cooldownState(); + case SwingState_Ready: + return update_readyState(); + case SwingState_Swing: + return update_swingState(); + case SwingState_Impact: + return update_impactState(); + case SwingState_Launch: + return update_launchState(); + default: + throw std::logic_error(std::string("You forgot to implement state ") + stateToString(state) + " ya dingus"); + } + } + + void StateMachine::update_cooldownState() + { + if (timeSinceEnteredState >= minimumPeriod) + transition_cooldownToReady(); + } + + void StateMachine::transition_cooldownToReady() + { + transition(SwingState_Ready); + } + + void StateMachine::update_readyState() + { + if (canSwing()) + return transition_readyToLaunch(); + } + + void StateMachine::transition_readyToLaunch() + { + transition(SwingState_Launch); + } + + void StateMachine::playSwish() + { + MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + + std::string sound = "Weapon Swish"; + if (strength < 0.5f) + sndMgr->playSound3D(ptr, sound, 1.0f, 0.8f); //Weak attack + if (strength < 1.0f) + sndMgr->playSound3D(ptr, sound, 1.0f, 1.0f); //Medium attack + else + sndMgr->playSound3D(ptr, sound, 1.0f, 1.2f); //Strong attack + + Log(Debug::Verbose) << "Swing: " << swingTypeToString(swingType); + } + + void StateMachine::update_launchState() + { + if (movementSinceEnteredState > minimumPeriod) + transition_launchToSwing(); + if (!canSwing()) + return transition_launchToReady(); + } + + void StateMachine::transition_launchToReady() + { + transition(SwingState_Ready); + } + + void StateMachine::transition_launchToSwing() + { + playSwish(); + transition(SwingState_Swing); + + // As a special case, update the new state immediately to allow + // same-frame impacts. + update_swingState(); + } + + void StateMachine::update_swingState() + { + maxSwingVelocity = std::max(velocity, maxSwingVelocity); + strength = std::min(1.f, (maxSwingVelocity - minVelocity) / maxVelocity); + + // When velocity falls below minimum, transition to register the miss + if (!canSwing()) + return transition_swingingToImpact(); + // Call hit with simulated=true to check for hit without actually causing an impact + if (ptr.getClass().hit(ptr, strength, swingType, true)) + return transition_swingingToImpact(); + } + + void StateMachine::transition_swingingToImpact() + { + ptr.getClass().hit(ptr, strength, swingType, false); + transition(SwingState_Impact); + } + + void StateMachine::update_impactState() + { + if (velocity < minVelocity) + return transition_impactToCooldown(); + } + + void StateMachine::transition_impactToCooldown() + { + transition(SwingState_Cooldown); } - } - switch (state) - { - case SwingState_Cooldown: - return update_cooldownState(); - case SwingState_Ready: - return update_readyState(); - case SwingState_Swing: - return update_swingState(); - case SwingState_Impact: - return update_impactState(); - case SwingState_Launch: - return update_launchState(); - default: - throw std::logic_error(std::string("You forgot to implement state ") + stateToString(state) + " ya dingus"); } } - -void StateMachine::update_cooldownState() -{ - if (timeSinceEnteredState >= minimumPeriod) - transition_cooldownToReady(); -} - -void StateMachine::transition_cooldownToReady() -{ - transition(SwingState_Ready); -} - -void StateMachine::update_readyState() -{ - if (canSwing()) - return transition_readyToLaunch(); -} - -void StateMachine::transition_readyToLaunch() -{ - transition(SwingState_Launch); -} - -void StateMachine::playSwish() -{ - MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); - - std::string sound = "Weapon Swish"; - if (strength < 0.5f) - sndMgr->playSound3D(ptr, sound, 1.0f, 0.8f); //Weak attack - if (strength < 1.0f) - sndMgr->playSound3D(ptr, sound, 1.0f, 1.0f); //Medium attack - else - sndMgr->playSound3D(ptr, sound, 1.0f, 1.2f); //Strong attack - - Log(Debug::Verbose) << "Swing: " << swingTypeToString(swingType); -} - -void StateMachine::update_launchState() -{ - if (movementSinceEnteredState > minimumPeriod) - transition_launchToSwing(); - if (!canSwing()) - return transition_launchToReady(); -} - -void StateMachine::transition_launchToReady() -{ - transition(SwingState_Ready); -} - -void StateMachine::transition_launchToSwing() -{ - playSwish(); - transition(SwingState_Swing); - - // As a special case, update the new state immediately to allow - // same-frame impacts. - update_swingState(); -} - -void StateMachine::update_swingState() -{ - maxSwingVelocity = std::max(velocity, maxSwingVelocity); - strength = std::min(1.f, (maxSwingVelocity - minVelocity) / maxVelocity); - - // When velocity falls below minimum, transition to register the miss - if (!canSwing()) - return transition_swingingToImpact(); - // Call hit with simulated=true to check for hit without actually causing an impact - if(ptr.getClass().hit(ptr, strength, swingType, true)) - return transition_swingingToImpact(); -} - -void StateMachine::transition_swingingToImpact() -{ - ptr.getClass().hit(ptr, strength, swingType, false); - transition(SwingState_Impact); -} - -void StateMachine::update_impactState() -{ - if (velocity < minVelocity) - return transition_impactToCooldown(); -} - -void StateMachine::transition_impactToCooldown() -{ - transition(SwingState_Cooldown); -} - -}} diff --git a/apps/openmw/mwvr/realisticcombat.hpp b/apps/openmw/mwvr/realisticcombat.hpp index 13554e75c..853110498 100644 --- a/apps/openmw/mwvr/realisticcombat.hpp +++ b/apps/openmw/mwvr/realisticcombat.hpp @@ -10,71 +10,73 @@ #include "vrenvironment.hpp" #include "vrsession.hpp" -namespace MWVR { namespace RealisticCombat { -enum SwingState -{ - SwingState_Ready, - SwingState_Launch, - SwingState_Swing, - SwingState_Impact, - SwingState_Cooldown, -}; +namespace MWVR { + namespace RealisticCombat { + enum SwingState + { + SwingState_Ready, + SwingState_Launch, + SwingState_Swing, + SwingState_Impact, + SwingState_Cooldown, + }; -struct StateMachine -{ - // TODO: These should be configurable - const float minVelocity = 1.f; - const float maxVelocity = 4.f; + struct StateMachine + { + // TODO: These should be configurable + const float minVelocity = 1.f; + const float maxVelocity = 4.f; - float velocity = 0.f; - float maxSwingVelocity = 0.f; + float velocity = 0.f; + float maxSwingVelocity = 0.f; - SwingState state = SwingState_Ready; - MWWorld::Ptr ptr = MWWorld::Ptr(); - int swingType = -1; - float strength = 0.f; + SwingState state = SwingState_Ready; + MWWorld::Ptr ptr = MWWorld::Ptr(); + int swingType = -1; + float strength = 0.f; - float thrustVelocity{ 0.f }; - float slashChopVelocity{ 0.f }; - float sideVelocity{ 0.f }; + float thrustVelocity{ 0.f }; + float slashChopVelocity{ 0.f }; + float sideVelocity{ 0.f }; - float minimumPeriod{ .25f }; + float minimumPeriod{ .25f }; - float timeSinceEnteredState = { 0.f }; - float movementSinceEnteredState = { 0.f }; + float timeSinceEnteredState = { 0.f }; + float movementSinceEnteredState = { 0.f }; - bool mEnabled = false; + bool mEnabled = false; - osg::Vec3 previousPosition{ 0.f,0.f,0.f }; + osg::Vec3 previousPosition{ 0.f,0.f,0.f }; - StateMachine(MWWorld::Ptr ptr); + StateMachine(MWWorld::Ptr ptr); - bool canSwing(); + bool canSwing(); - void playSwish(); - void reset(); + void playSwish(); + void reset(); - void transition(SwingState newState); + void transition(SwingState newState); - void update(float dt, bool enabled); + void update(float dt, bool enabled); - void update_cooldownState(); - void transition_cooldownToReady(); + void update_cooldownState(); + void transition_cooldownToReady(); - void update_readyState(); - void transition_readyToLaunch(); + void update_readyState(); + void transition_readyToLaunch(); - void update_launchState(); - void transition_launchToReady(); - void transition_launchToSwing(); + void update_launchState(); + void transition_launchToReady(); + void transition_launchToSwing(); - void update_swingState(); - void transition_swingingToImpact(); + void update_swingState(); + void transition_swingingToImpact(); - void update_impactState(); - void transition_impactToCooldown(); -}; + void update_impactState(); + void transition_impactToCooldown(); + }; -}} + } +} #endif diff --git a/apps/openmw/mwvr/vranimation.cpp b/apps/openmw/mwvr/vranimation.cpp index 2891aa4e1..c294589ba 100644 --- a/apps/openmw/mwvr/vranimation.cpp +++ b/apps/openmw/mwvr/vranimation.cpp @@ -33,689 +33,654 @@ namespace MWVR { + // Some weapon types, such as spellcast, are classified as melee even though they are not. At least not in the way i want. + // All the false melee types have negative enum values, but also so does hand to hand. + // I think this covers all cases + static bool isMeleeWeapon(int type) + { + if (MWMechanics::getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee) + return false; + if (type == ESM::Weapon::HandToHand) + return true; + if (type >= 0) + return true; -// Some weapon types, such as spellcast, are classified as melee even though they are not. -// All the fake melee types have negative type enum, but also so does hand to hand. -// I think this covers all the cases -static bool isMeleeWeapon(int type) -{ - if (MWMechanics::getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee) return false; - if (type == ESM::Weapon::HandToHand) - return true; - if (type >= 0) - return true; + } - return false; -} - -/// Implements VR control of the forearm, to control mesh/bone deformation of the hand. -class ForearmController : public osg::NodeCallback -{ -public: - ForearmController() = default; - void setEnabled(bool enabled) { mEnabled = enabled; }; - void operator()(osg::Node* node, osg::NodeVisitor* nv); - -private: - bool mEnabled{ true }; -}; - -void ForearmController::operator()(osg::Node* node, osg::NodeVisitor* nv) -{ - if (!mEnabled) + /// Implements VR control of the forearm, to control mesh/bone deformation of the hand. + class ForearmController : public osg::NodeCallback { + public: + ForearmController() = default; + void setEnabled(bool enabled) { mEnabled = enabled; }; + void operator()(osg::Node* node, osg::NodeVisitor* nv); + + private: + bool mEnabled{ true }; + }; + + void ForearmController::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (!mEnabled) + { + traverse(node, nv); + return; + } + + osg::MatrixTransform* transform = static_cast(node); + auto* camera = MWBase::Environment::get().getWorld()->getRenderingManager().getCamera(); + + auto* session = Environment::get().getSession(); + int side = (int)Side::RIGHT_SIDE; + if (node->getName().find_first_of("L") != std::string::npos) + { + side = (int)Side::LEFT_SIDE; + // We base ourselves on the world position of the camera + // Therefore we have to make sure the camera is updated for this frame first. + camera->updateCamera(); + } + + MWVR::Pose handStage = session->predictedPoses(VRSession::FramePhase::Update).hands[side]; + MWVR::Pose headStage = session->predictedPoses(VRSession::FramePhase::Update).head; + + auto orientation = handStage.orientation; + + auto position = handStage.position - headStage.position; + position = position * Environment::get().unitsPerMeter(); + + // Align orientation with the game world + auto* inputManager = Environment::get().getInputManager(); + if (inputManager) + { + auto stageRotation = inputManager->stageRotation(); + position = stageRotation * position; + orientation = orientation * stageRotation; + } + + // Add camera offset + osg::Vec3 viewPosition; + osg::Vec3 center; // dummy + osg::Vec3 up; // dummy + + auto viewMatrix = camera->getOsgCamera()->getViewMatrix(); + viewMatrix.getLookAt(viewPosition, center, up, 1.0); + position += viewPosition; + + // Morrowind's meshes do not point forward by default. + // Declare the offsets static since they do not need to be recomputed. + static float VRbias = osg::DegreesToRadians(-90.f); + static osg::Quat yaw(-VRbias, osg::Vec3f(0, 0, 1)); + static osg::Quat pitch(2.f * VRbias, osg::Vec3f(0, 1, 0)); + static osg::Quat roll(2 * VRbias, osg::Vec3f(1, 0, 0)); + orientation = yaw * orientation; + if (side == (int)Side::LEFT_SIDE) + orientation = roll * orientation; + + // Undo the wrist translate + auto* hand = transform->getChild(0); + auto handMatrix = hand->asTransform()->asMatrixTransform()->getMatrix(); + position -= orientation * handMatrix.getTrans(); + + // Center hand mesh on tracking + // This is just an estimate from trial and error, any suggestion for improving this is welcome + position -= orientation * osg::Vec3{ 15,0,0 }; + + // Get current world transform of limb + osg::Matrix worldToLimb = osg::computeLocalToWorld(node->getParentalNodePaths()[0]); + // Get current world of the reference node + osg::Matrix worldReference = osg::Matrix::identity(); + // New transform based on tracking. + worldReference.preMultTranslate(position); + worldReference.preMultRotate(orientation); + + // Finally, set transform + transform->setMatrix(worldReference * osg::Matrix::inverse(worldToLimb) * transform->getMatrix()); + + // Omit nested callbacks to override animations of this node + osg::ref_ptr ncb = getNestedCallback(); + setNestedCallback(nullptr); traverse(node, nv); - return; + setNestedCallback(ncb); } - osg::MatrixTransform* transform = static_cast(node); - - auto* session = Environment::get().getSession(); - auto* xrViewer = Environment::get().getViewer(); - int side = (int)Side::RIGHT_SIDE; - if (node->getName().find_first_of("L") != std::string::npos) + /// Implements control of a finger by overriding rotation + class FingerController : public osg::NodeCallback { - side = (int)Side::LEFT_SIDE; - // We base ourselves on the world position of the camera - // Ensure it is updated before placing the hands - // I'm sure this can be achieved properly by managing the scene graph better. - MWBase::Environment::get().getWorld()->getRenderingManager().getCamera()->updateCamera(); - } + public: + FingerController() {}; + void setEnabled(bool enabled) { mEnabled = enabled; }; + void operator()(osg::Node* node, osg::NodeVisitor* nv); - MWVR::Pose handStage = session->predictedPoses(VRSession::FramePhase::Update).hands[side]; - MWVR::Pose headStage = session->predictedPoses(VRSession::FramePhase::Update).head; + private: + bool mEnabled = true; + }; - auto orientation = handStage.orientation; - - auto position = handStage.position - headStage.position; - position = position * Environment::get().unitsPerMeter(); - - auto camera = xrViewer->mViewer->getCamera(); - auto viewMatrix = camera->getViewMatrix(); - - - // Align orientation with the game world - auto* inputManager = Environment::get().getInputManager(); - if (inputManager) + void FingerController::operator()(osg::Node* node, osg::NodeVisitor* nv) { - auto stageRotation = inputManager->stageRotation(); - position = stageRotation * position; - orientation = orientation * stageRotation; - } + if (!mEnabled) + { + traverse(node, nv); + return; + } - // Add camera offset - osg::Vec3 viewPosition; - osg::Vec3 center; - osg::Vec3 up; + // Since morrowinds assets do not do a particularly good job of imitating natural hands and poses, + // the best i can do for pointing is to just make it point straight forward. While not + // totally natural, it works. + osg::Quat rotate{ 0,0,0,1 }; + auto matrixTransform = node->asTransform()->asMatrixTransform(); + auto matrix = matrixTransform->getMatrix(); + matrix.setRotate(rotate); + matrixTransform->setMatrix(matrix); - viewMatrix.getLookAt(viewPosition, center, up, 1.0); - position += viewPosition; - //// Morrowind's meshes do not point forward by default. - //// Declare the offsets static since they do not need to be recomputed. - static float VRbias = osg::DegreesToRadians(-90.f); - static osg::Quat yaw(-VRbias, osg::Vec3f(0, 0, 1)); - static osg::Quat pitch(2.f * VRbias, osg::Vec3f(0, 1, 0)); - static osg::Quat roll(2 * VRbias, osg::Vec3f(1, 0, 0)); - orientation = yaw * orientation; - if (side == (int)Side::LEFT_SIDE) - orientation = roll * orientation; - - // Undo the wrist translate - auto* hand = transform->getChild(0); - auto handMatrix = hand->asTransform()->asMatrixTransform()->getMatrix(); - position -= orientation * handMatrix.getTrans(); - - // Center hand mesh on tracking - // This is just an estimate from trial and error, any suggestion for improving this is welcome - position -= orientation * osg::Vec3{ 15,0,0 }; - - // Get current world transform of limb - osg::Matrix worldToLimb = osg::computeLocalToWorld(node->getParentalNodePaths()[0]); - // Get current world of the reference node - osg::Matrix worldReference = osg::Matrix::identity(); - // New transform based on tracking. - worldReference.preMultTranslate(position); - worldReference.preMultRotate(orientation); - - // Finally, set transform - transform->setMatrix(worldReference * osg::Matrix::inverse(worldToLimb) * transform->getMatrix()); - - // Omit nested callbacks to override animations of this node - osg::ref_ptr ncb = getNestedCallback(); - setNestedCallback(nullptr); - traverse(node, nv); - setNestedCallback(ncb); -} - -/// Implements control of a finger by overriding rotation -class FingerController : public osg::NodeCallback -{ -public: - FingerController() {}; - void setEnabled(bool enabled) { mEnabled = enabled; }; - void operator()(osg::Node* node, osg::NodeVisitor* nv); - -private: - bool mEnabled = true; -}; - -void FingerController::operator()(osg::Node* node, osg::NodeVisitor* nv) -{ - if (!mEnabled) - { + // Omit nested callbacks to override animations of this node + osg::ref_ptr ncb = getNestedCallback(); + setNestedCallback(nullptr); traverse(node, nv); - return; + setNestedCallback(ncb); + + // Recompute pointer target + auto* anim = MWVR::Environment::get().getPlayerAnimation(); + if (anim && node->getName() == "Bip01 R Finger1") + { + anim->updatePointerTarget(); + } } - - osg::Quat rotate{ 0,0,0,1 }; - - // TODO: - // To make finger pointing more natural each joint should angle down roughly 5-6 degrees per joint. - // But this creates obvious visual conflicts with the hand and the rest of the fingers which are not posed naturally, - // and looks particularly riddiculous when a weapon is drawn. - // Therefore, for the time being i will simply have them point straight forward relative to the current hand rotation. - // This leads to particularly awkward finger pointing while a weapon is drawn and should be replaced by a - // complete override of all hand animations by the end of 2090. - - //////// Add a slight rotation down of the fingers to naturalize the pointing. - //////rotate = osg::Quat(osg::PI_4 / 8, osg::Vec3{ 0,1,0 }) * rotate; - //////if (node->getName() == "Bip01 R Finger1") - //////{ - ////// auto* world = MWBase::Environment::get().getWorld(); - - ////// // Morrowind models do not hold weapons at a natural angle, so i rotate the hand forward - ////// // to get a more natural angle on the weapon to allow more comfortable combat. - ////// // Fingers need to angle back up to keep pointing natural. - ////// if (world->getActiveWeaponType() >= 0) - ////// { - ////// rotate = osg::Quat(-osg::PI_4, osg::Vec3{ 0,1,0 }) * rotate; - ////// } - //////} - - // First, update the base of the finger to the overriding orientation - auto matrixTransform = node->asTransform()->asMatrixTransform(); - auto matrix = matrixTransform->getMatrix(); - matrix.setRotate(rotate); - matrixTransform->setMatrix(matrix); - - - // Omit nested callbacks to override animations of this node - osg::ref_ptr ncb = getNestedCallback(); - setNestedCallback(nullptr); - traverse(node, nv); - setNestedCallback(ncb); - - // Update where the player is currently pointing - auto* anim = MWVR::Environment::get().getPlayerAnimation(); - if (anim && node->getName() == "Bip01 R Finger1") + /// Implements control of a finger by overriding rotation + class HandController : public osg::NodeCallback { - anim->updatePointerTarget(); - } -} + public: + HandController() = default; + void setEnabled(bool enabled) { mEnabled = enabled; }; + void operator()(osg::Node* node, osg::NodeVisitor* nv); -/// Implements control of a finger by overriding rotation -class HandController : public osg::NodeCallback -{ -public: - HandController() = default; - void setEnabled(bool enabled) { mEnabled = enabled; }; - void operator()(osg::Node* node, osg::NodeVisitor* nv); + private: + bool mEnabled = true; + }; -private: - bool mEnabled = true; -}; - -void HandController::operator()(osg::Node* node, osg::NodeVisitor* nv) -{ - if (!mEnabled) + void HandController::operator()(osg::Node* node, osg::NodeVisitor* nv) { + if (!mEnabled) + { + traverse(node, nv); + return; + } + float PI_2 = osg::PI_2; + if (node->getName() == "Bip01 L Hand") + PI_2 = -PI_2; + float PI_4 = PI_2 / 2.f; + + osg::Quat rotate{ 0,0,0,1 }; + auto* world = MWBase::Environment::get().getWorld(); + auto windowManager = MWBase::Environment::get().getWindowManager(); + auto animation = MWVR::Environment::get().getPlayerAnimation(); + auto weaponType = world->getActiveWeaponType(); + // Morrowind models do not hold most weapons at a natural angle, so i rotate the hand + // to more natural angles on weapons to allow more comfortable combat. + if (!windowManager->isGuiMode() && !animation->fingerPointingMode()) + { + + switch (weaponType) + { + case ESM::Weapon::None: + case ESM::Weapon::HandToHand: + case ESM::Weapon::MarksmanThrown: + case ESM::Weapon::Spell: + case ESM::Weapon::Arrow: + case ESM::Weapon::Bolt: + // No adjustment + break; + case ESM::Weapon::MarksmanCrossbow: + // Crossbow points upwards. Assumedly because i am overriding hand animations. + rotate = osg::Quat(PI_4 / 1.05, osg::Vec3{ 0,1,0 }) * osg::Quat(0.06, osg::Vec3{ 0,0,1 }); + break; + case ESM::Weapon::MarksmanBow: + // Bow points down by default, rotate it back up a little + rotate = osg::Quat(-PI_2 * .10f, osg::Vec3{ 0,1,0 }); + break; + default: + // Melee weapons Need adjustment + rotate = osg::Quat(PI_4, osg::Vec3{ 0,1,0 }); + break; + } + } + + auto matrixTransform = node->asTransform()->asMatrixTransform(); + auto matrix = matrixTransform->getMatrix(); + matrix.setRotate(rotate); + matrixTransform->setMatrix(matrix); + + + // Omit nested callbacks to override animations of this node + osg::ref_ptr ncb = getNestedCallback(); + setNestedCallback(nullptr); traverse(node, nv); - return; + setNestedCallback(ncb); } - float PI_2 = osg::PI_2; - if (node->getName() == "Bip01 L Hand") - PI_2 = -PI_2; - float PI_4 = PI_2 / 2.f; - osg::Quat rotate{ 0,0,0,1 }; - auto* world = MWBase::Environment::get().getWorld(); - auto windowManager = MWBase::Environment::get().getWindowManager(); - auto animation = MWVR::Environment::get().getPlayerAnimation(); - auto weaponType = world->getActiveWeaponType(); - // Morrowind models do not hold most weapons at a natural angle, so i rotate the hand - // to more natural angles on weapons to allow more comfortable combat. - if (!windowManager->isGuiMode() && !animation->fingerPointingMode()) + /// Implements control of weapon direction + class WeaponDirectionController : public osg::NodeCallback { + public: + WeaponDirectionController() = default; + void setEnabled(bool enabled) { mEnabled = enabled; }; + void operator()(osg::Node* node, osg::NodeVisitor* nv); + private: + bool mEnabled = true; + }; + + void WeaponDirectionController::operator()(osg::Node* node, osg::NodeVisitor* nv) + { + if (!mEnabled) + { + traverse(node, nv); + return; + } + + // Arriving here implies a parent, no need to check + auto parent = static_cast(node->getParent(0)); + + + + osg::Quat rotate{ 0,0,0,1 }; + auto* world = MWBase::Environment::get().getWorld(); + auto weaponType = world->getActiveWeaponType(); switch (weaponType) { - case ESM::Weapon::None: - case ESM::Weapon::HandToHand: case ESM::Weapon::MarksmanThrown: case ESM::Weapon::Spell: case ESM::Weapon::Arrow: case ESM::Weapon::Bolt: - // No adjustment - break; - case ESM::Weapon::MarksmanCrossbow: - // Crossbow points upwards. Assumedly because i am overriding hand animations. - rotate = osg::Quat(PI_4 / 1.05, osg::Vec3{ 0,1,0 }) * osg::Quat(0.06, osg::Vec3{ 0,0,1 }); - break; + case ESM::Weapon::HandToHand: case ESM::Weapon::MarksmanBow: - // Bow points down by default, rotate it back up a little - rotate = osg::Quat(-PI_2 * .10f, osg::Vec3{ 0,1,0 }); + case ESM::Weapon::MarksmanCrossbow: + // Rotate to point straight forward, reverting any rotation of the hand to keep aim consistent. + rotate = parent->getInverseMatrix().getRotate(); + rotate = osg::Quat(-osg::PI_2, osg::Vec3{ 0,0,1 }) * rotate; break; default: - // Melee weapons Need adjustment - rotate = osg::Quat(PI_4, osg::Vec3{ 0,1,0 }); + // Melee weapons point straight up from the hand + rotate = osg::Quat(osg::PI_2, osg::Vec3{ 1,0,0 }); break; } - } - auto matrixTransform = node->asTransform()->asMatrixTransform(); - auto matrix = matrixTransform->getMatrix(); - matrix.setRotate(rotate); - matrixTransform->setMatrix(matrix); + auto matrixTransform = node->asTransform()->asMatrixTransform(); + auto matrix = matrixTransform->getMatrix(); + matrix.setRotate(rotate); + matrixTransform->setMatrix(matrix); - - // Omit nested callbacks to override animations of this node - osg::ref_ptr ncb = getNestedCallback(); - setNestedCallback(nullptr); - traverse(node, nv); - setNestedCallback(ncb); -} - -/// Implements control of weapon direction -class WeaponDirectionController : public osg::NodeCallback -{ -public: - WeaponDirectionController() = default; - void setEnabled(bool enabled) { mEnabled = enabled; }; - void operator()(osg::Node* node, osg::NodeVisitor* nv); - -private: - bool mEnabled = true; -}; - -void WeaponDirectionController::operator()(osg::Node* node, osg::NodeVisitor* nv) -{ - if (!mEnabled) - { traverse(node, nv); - return; } - // Arriving here implies a parent, no need to check - auto parent = static_cast(node->getParent(0)); - - - - osg::Quat rotate{ 0,0,0,1 }; - auto* world = MWBase::Environment::get().getWorld(); - auto weaponType = world->getActiveWeaponType(); - switch (weaponType) + /// Implements control of the weapon pointer + class WeaponPointerController : public osg::NodeCallback { - case ESM::Weapon::MarksmanThrown: - case ESM::Weapon::Spell: - case ESM::Weapon::Arrow: - case ESM::Weapon::Bolt: - case ESM::Weapon::HandToHand: - case ESM::Weapon::MarksmanBow: - case ESM::Weapon::MarksmanCrossbow: - // Rotate to point straight forward, reverting any rotation of the hand to keep aim consistent. - rotate = parent->getInverseMatrix().getRotate(); - rotate = osg::Quat(-osg::PI_2, osg::Vec3{ 0,0,1 }) * rotate; - break; - default: - // Melee weapons point straight up from the hand - rotate = osg::Quat(osg::PI_2, osg::Vec3{ 1,0,0 }); - break; - } + public: + WeaponPointerController() = default; + void setEnabled(bool enabled) { mEnabled = enabled; }; + void operator()(osg::Node* node, osg::NodeVisitor* nv); - auto matrixTransform = node->asTransform()->asMatrixTransform(); - auto matrix = matrixTransform->getMatrix(); - matrix.setRotate(rotate); - matrixTransform->setMatrix(matrix); + private: + bool mEnabled = true; + }; - traverse(node, nv); -} - -/// Implements control of the weapon pointer -class WeaponPointerController : public osg::NodeCallback -{ -public: - WeaponPointerController() = default; - void setEnabled(bool enabled) { mEnabled = enabled; }; - void operator()(osg::Node* node, osg::NodeVisitor* nv); - -private: - bool mEnabled = true; -}; - -void WeaponPointerController::operator()(osg::Node* node, osg::NodeVisitor* nv) -{ - if (!mEnabled) + void WeaponPointerController::operator()(osg::Node* node, osg::NodeVisitor* nv) { + if (!mEnabled) + { + traverse(node, nv); + return; + } + + auto matrixTransform = node->asTransform()->asMatrixTransform(); + auto world = MWBase::Environment::get().getWorld(); + auto weaponType = world->getActiveWeaponType(); + auto windowManager = MWBase::Environment::get().getWindowManager(); + + if (!isMeleeWeapon(weaponType) && !windowManager->isGuiMode()) + { + // Ranged weapons should show a pointer to where they are targeting + matrixTransform->setMatrix( + osg::Matrix::scale(1.f, 64.f, 1.f) + ); + } + else + { + matrixTransform->setMatrix( + osg::Matrix::scale(1.f, 64.f, 1.f) + ); + } + + // First, update the base of the finger to the overriding orientation + traverse(node, nv); + } + + VRAnimation::VRAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, + bool disableSounds, std::shared_ptr xrSession) + // Note that i let it construct as 3rd person and then later update it to VM_VRFirstPerson + // when the character controller updates + : MWRender::NpcAnimation(ptr, parentNode, resourceSystem, disableSounds, VM_Normal, 55.f) + , mSession(xrSession) + , mIndexFingerControllers{ nullptr, nullptr } + // The player model needs to be pushed back a little to make sure the player's view point is naturally protruding + // Pushing the camera forward instead would produce an unnatural extra movement when rotating the player model. + , mModelOffset(new osg::MatrixTransform(osg::Matrix::translate(osg::Vec3(0, -15, 0)))) + { + for (int i = 0; i < 2; i++) + { + mIndexFingerControllers[i] = new FingerController; + mForearmControllers[i] = new ForearmController; + mHandControllers[i] = new HandController; + } + + mWeaponDirectionTransform = new osg::MatrixTransform(); + mWeaponDirectionTransform->setName("Weapon Direction"); + mWeaponDirectionTransform->setUpdateCallback(new WeaponDirectionController); + + mModelOffset->setName("ModelOffset"); + mPointerGeometry = createPointerGeometry(); + mPointerRescale = new osg::MatrixTransform(); + mPointerRescale->addChild(mPointerGeometry); + mPointerTransform = new osg::MatrixTransform(); + mPointerTransform->addChild(mPointerRescale); + mPointerTransform->setName("Pointer Transform"); + // Morrowind's hands don't actually point forward, so we have to reorient the pointer. + mPointerTransform->setMatrix(osg::Matrix::rotate(osg::Quat(-osg::PI_2, osg::Vec3f(0, 0, 1)))); + + mWeaponPointerTransform = new osg::MatrixTransform(); + mWeaponPointerTransform->addChild(mPointerGeometry); + mWeaponPointerTransform->setMatrix( + osg::Matrix::scale(0.f, 0.f, 0.f) + ); + mWeaponPointerTransform->setName("Weapon Pointer"); + mWeaponPointerTransform->setUpdateCallback(new WeaponPointerController); + //mWeaponDirectionTransform->addChild(mWeaponPointerTransform); + } + + VRAnimation::~VRAnimation() {}; + + void VRAnimation::setViewMode(NpcAnimation::ViewMode viewMode) + { + if (viewMode != VM_VRFirstPerson && viewMode != VM_VRNormal) + { + Log(Debug::Warning) << "Attempted to set view mode of VRAnimation to non-vr mode. Defaulted to VM_VRFirstPerson."; + viewMode = VM_VRFirstPerson; + } + NpcAnimation::setViewMode(viewMode); return; } - auto matrixTransform = node->asTransform()->asMatrixTransform(); - auto world = MWBase::Environment::get().getWorld(); - auto weaponType = world->getActiveWeaponType(); - auto windowManager = MWBase::Environment::get().getWindowManager(); + void VRAnimation::updateParts() + { + NpcAnimation::updateParts(); - if (!isMeleeWeapon(weaponType) && !windowManager->isGuiMode()) - { - // Ranged weapons should show a pointer to where they are targeting - matrixTransform->setMatrix( - osg::Matrix::scale(1.f, 64.f, 1.f) - ); - } - else - { - matrixTransform->setMatrix( - osg::Matrix::scale(1.f, 64.f, 1.f) - ); + if (mViewMode == VM_VRFirstPerson) + { + // Hide everything other than the hands and feet. + removeIndividualPart(ESM::PartReferenceType::PRT_Hair); + removeIndividualPart(ESM::PartReferenceType::PRT_Head); + removeIndividualPart(ESM::PartReferenceType::PRT_LForearm); + removeIndividualPart(ESM::PartReferenceType::PRT_LUpperarm); + removeIndividualPart(ESM::PartReferenceType::PRT_LWrist); + removeIndividualPart(ESM::PartReferenceType::PRT_RForearm); + removeIndividualPart(ESM::PartReferenceType::PRT_RUpperarm); + removeIndividualPart(ESM::PartReferenceType::PRT_RWrist); + removeIndividualPart(ESM::PartReferenceType::PRT_Cuirass); + removeIndividualPart(ESM::PartReferenceType::PRT_Groin); + removeIndividualPart(ESM::PartReferenceType::PRT_Neck); + removeIndividualPart(ESM::PartReferenceType::PRT_Skirt); + removeIndividualPart(ESM::PartReferenceType::PRT_Tail); + removeIndividualPart(ESM::PartReferenceType::PRT_LLeg); + removeIndividualPart(ESM::PartReferenceType::PRT_RLeg); + removeIndividualPart(ESM::PartReferenceType::PRT_LAnkle); + removeIndividualPart(ESM::PartReferenceType::PRT_RAnkle); + removeIndividualPart(ESM::PartReferenceType::PRT_LKnee); + removeIndividualPart(ESM::PartReferenceType::PRT_RKnee); + removeIndividualPart(ESM::PartReferenceType::PRT_LFoot); + removeIndividualPart(ESM::PartReferenceType::PRT_RFoot); + } + else + { + removeIndividualPart(ESM::PartReferenceType::PRT_LForearm); + removeIndividualPart(ESM::PartReferenceType::PRT_LWrist); + removeIndividualPart(ESM::PartReferenceType::PRT_RForearm); + removeIndividualPart(ESM::PartReferenceType::PRT_RWrist); + } + + + auto playerPtr = MWMechanics::getPlayer(); + const MWWorld::LiveCellRef* ref = playerPtr.get(); + const ESM::Race* race = + MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); + bool isMale = ref->mBase->isMale(); + float charHeightFactor = isMale ? race->mData.mHeight.mMale : race->mData.mHeight.mFemale; + float charHeightBase = 1.8f; + float charHeight = charHeightBase * charHeightFactor; + // TODO: Player height should be configurable + // For now i'm just using my own + float sizeFactor = 1.85f / charHeight; + Environment::get().getSession()->setPlayerScale(sizeFactor); } - // First, update the base of the finger to the overriding orientation - - traverse(node, nv); -} - -VRAnimation::VRAnimation( - const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, - bool disableSounds, std::shared_ptr xrSession) - // Note that i let it construct as 3rd person and then later update it to VM_VRHeadless - // when the character controller updates - : MWRender::NpcAnimation(ptr, parentNode, resourceSystem, disableSounds, VM_Normal, 55.f) - , mSession(xrSession) - , mIndexFingerControllers{nullptr, nullptr} - // The player model needs to be pushed back a little to make sure the player's view point is naturally protruding - // Pushing the camera forward instead would produce an unnatural extra movement when rotating the player model. - , mModelOffset(new osg::MatrixTransform(osg::Matrix::translate(osg::Vec3(0,-15,0)))) -{ - for (int i = 0; i < 2; i++) + void VRAnimation::setFingerPointingMode(bool enabled) { - mIndexFingerControllers[i] = new FingerController; - mForearmControllers[i] = new ForearmController; - mHandControllers[i] = new HandController; - } + if (enabled == mFingerPointingMode) + return; - mWeaponDirectionTransform = new osg::MatrixTransform(); - mWeaponDirectionTransform->setName("Weapon Direction"); - mWeaponDirectionTransform->setUpdateCallback(new WeaponDirectionController); + auto finger = mNodeMap.find("bip01 r finger1"); + if (finger != mNodeMap.end()) + { + auto base_joint = finger->second; + auto second_joint = base_joint->getChild(0)->asTransform()->asMatrixTransform(); + assert(second_joint); - mModelOffset->setName("ModelOffset"); - mPointerGeometry = createPointerGeometry(); - mPointerRescale = new osg::MatrixTransform(); - mPointerRescale->addChild(mPointerGeometry); - mPointerTransform = new osg::MatrixTransform(); - mPointerTransform->addChild(mPointerRescale); - mPointerTransform->setName("Pointer Transform"); - // Morrowind's hands don't actually point forward, so we have to reorient the pointer. - mPointerTransform->setMatrix(osg::Matrix::rotate(osg::Quat(-osg::PI_2, osg::Vec3f(0, 0, 1)))); + base_joint->removeUpdateCallback(mIndexFingerControllers[0]); + second_joint->removeUpdateCallback(mIndexFingerControllers[1]); + if (enabled) + { + base_joint->addUpdateCallback(mIndexFingerControllers[0]); + second_joint->addUpdateCallback(mIndexFingerControllers[1]); - mWeaponPointerTransform = new osg::MatrixTransform(); - mWeaponPointerTransform->addChild(mPointerGeometry); - mWeaponPointerTransform->setMatrix( - osg::Matrix::scale(0.f, 0.f, 0.f) - ); - mWeaponPointerTransform->setName("Weapon Pointer"); - mWeaponPointerTransform->setUpdateCallback(new WeaponPointerController); - //mWeaponDirectionTransform->addChild(mWeaponPointerTransform); -} + } + } -VRAnimation::~VRAnimation() {}; - -void VRAnimation::setViewMode(NpcAnimation::ViewMode viewMode) -{ - if (viewMode != VM_VRFirstPerson && viewMode != VM_VRNormal) - { - Log(Debug::Warning) << "Attempted to set view mode of VRAnimation to non-vr mode. Defaulted to VM_VRFirstPerson."; - viewMode = VM_VRFirstPerson; - } - NpcAnimation::setViewMode(viewMode); - return; -} - -void VRAnimation::updateParts() -{ - NpcAnimation::updateParts(); - - if (mViewMode == VM_VRFirstPerson) - { - // Hide everything other than the hands and feet. - removeIndividualPart(ESM::PartReferenceType::PRT_Hair); - removeIndividualPart(ESM::PartReferenceType::PRT_Head); - removeIndividualPart(ESM::PartReferenceType::PRT_LForearm); - removeIndividualPart(ESM::PartReferenceType::PRT_LUpperarm); - removeIndividualPart(ESM::PartReferenceType::PRT_LWrist); - removeIndividualPart(ESM::PartReferenceType::PRT_RForearm); - removeIndividualPart(ESM::PartReferenceType::PRT_RUpperarm); - removeIndividualPart(ESM::PartReferenceType::PRT_RWrist); - removeIndividualPart(ESM::PartReferenceType::PRT_Cuirass); - removeIndividualPart(ESM::PartReferenceType::PRT_Groin); - removeIndividualPart(ESM::PartReferenceType::PRT_Neck); - removeIndividualPart(ESM::PartReferenceType::PRT_Skirt); - removeIndividualPart(ESM::PartReferenceType::PRT_Tail); - removeIndividualPart(ESM::PartReferenceType::PRT_LLeg); - removeIndividualPart(ESM::PartReferenceType::PRT_RLeg); - removeIndividualPart(ESM::PartReferenceType::PRT_LAnkle); - removeIndividualPart(ESM::PartReferenceType::PRT_RAnkle); - removeIndividualPart(ESM::PartReferenceType::PRT_LKnee); - removeIndividualPart(ESM::PartReferenceType::PRT_RKnee); - removeIndividualPart(ESM::PartReferenceType::PRT_LFoot); - removeIndividualPart(ESM::PartReferenceType::PRT_RFoot); - } - else - { - removeIndividualPart(ESM::PartReferenceType::PRT_LForearm); - removeIndividualPart(ESM::PartReferenceType::PRT_LWrist); - removeIndividualPart(ESM::PartReferenceType::PRT_RForearm); - removeIndividualPart(ESM::PartReferenceType::PRT_RWrist); - } - - - auto playerPtr = MWMechanics::getPlayer(); - const MWWorld::LiveCellRef* ref = playerPtr.get(); - const ESM::Race* race = - MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); - bool isMale = ref->mBase->isMale(); - float charHeightFactor = isMale ? race->mData.mHeight.mMale : race->mData.mHeight.mFemale; - float charHeightBase = 1.8f; - float charHeight = charHeightBase * charHeightFactor; - // TODO: Player height should be configurable - // For now i'm just using my own - float sizeFactor = 1.85f / charHeight; - Environment::get().getSession()->setPlayerScale(sizeFactor); -} - -void VRAnimation::setFingerPointingMode(bool enabled) -{ - if (enabled == mFingerPointingMode) - return; - - auto finger = mNodeMap.find("bip01 r finger1"); - if (finger != mNodeMap.end()) - { - auto base_joint = finger->second; - auto second_joint = base_joint->getChild(0)->asTransform()->asMatrixTransform(); - assert(second_joint); - - base_joint->removeUpdateCallback(mIndexFingerControllers[0]); - second_joint->removeUpdateCallback(mIndexFingerControllers[1]); + mPointerTransform->removeChild(mPointerRescale); if (enabled) { - base_joint->addUpdateCallback(mIndexFingerControllers[0]); - second_joint->addUpdateCallback(mIndexFingerControllers[1]); - + mPointerTransform->addChild(mPointerRescale); } - } - - mPointerTransform->removeChild(mPointerRescale); - if (enabled) - { - mPointerTransform->addChild(mPointerRescale); - } - else - { - mPointerTarget = MWRender::RayResult{}; - } - - mFingerPointingMode = enabled; -} - -osg::ref_ptr VRAnimation::createPointerGeometry(void) -{ - osg::ref_ptr geometry = new osg::Geometry(); - - // Create pointer geometry, which will point from the tip of the player's finger. - // The geometry will be a Four sided pyramid, with the top at the player's fingers - - osg::Vec3 vertices[]{ - {0, 0, 0}, // origin - {1, 1, -1}, // top_left - {-1, 1, -1}, // bottom_left - {-1, 1, 1}, // bottom_right - {1, 1, 1}, // top_right - }; - - osg::Vec4 colors[]{ - osg::Vec4(1.0f, 0.0f, 0.0f, 0.0f), - osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), - osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), - osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), - osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), - }; - - const int origin = 0; - const int top_left = 1; - const int bottom_left = 2; - const int bottom_right = 3; - const int top_right = 4; - - const int triangles[] = - { - bottom_right, top_right, top_left, - bottom_right, top_left, bottom_left, - origin, top_left, top_right, - origin, top_right, bottom_right, - origin, bottom_left, top_left, - origin, bottom_right, bottom_left, - }; - int numVertices = sizeof(triangles) / sizeof(*triangles); - osg::ref_ptr vertexArray = new osg::Vec3Array(numVertices); - osg::ref_ptr colorArray = new osg::Vec4Array(numVertices); - for (int i = 0; i < numVertices; i++) - { - (*vertexArray)[i] = vertices[triangles[i]]; - (*colorArray)[i] = colors[triangles[i]]; - } - - osg::ref_ptr normals = new osg::Vec3Array; - normals->push_back(osg::Vec3(0.0f, -1.0f, 0.0f)); - - geometry->setVertexArray(vertexArray); - geometry->setColorArray(colorArray, osg::Array::BIND_PER_VERTEX); - geometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, numVertices)); - geometry->setSupportsDisplayList(false); - geometry->setDataVariance(osg::Object::STATIC); - - auto stateset = geometry->getOrCreateStateSet(); - stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - stateset->setMode(GL_BLEND, osg::StateAttribute::ON); - stateset->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - - return geometry; -} - -float VRAnimation::getVelocity(const std::string& groupname) const -{ - return 0.0f; -} - -osg::Vec3f VRAnimation::runAnimation(float timepassed) -{ - return NpcAnimation::runAnimation(timepassed); -} - -void VRAnimation::addControllers() -{ - NpcAnimation::addControllers(); - - for (int i = 0; i < 2; ++i) - { - auto forearm = mNodeMap.find(i == 0 ? "bip01 l forearm" : "bip01 r forearm"); - if (forearm != mNodeMap.end()) + else { - auto node = forearm->second; - node->removeUpdateCallback(mForearmControllers[i]); - node->addUpdateCallback(mForearmControllers[i]); + mPointerTarget = MWRender::RayResult{}; } - auto hand = mNodeMap.find(i == 0 ? "bip01 l hand" : "bip01 r hand"); + mFingerPointingMode = enabled; + } + + osg::ref_ptr VRAnimation::createPointerGeometry(void) + { + osg::ref_ptr geometry = new osg::Geometry(); + + // Create pointer geometry, which will point from the tip of the player's finger. + // The geometry will be a Four sided pyramid, with the top at the player's fingers + + osg::Vec3 vertices[]{ + {0, 0, 0}, // origin + {1, 1, -1}, // top_left + {-1, 1, -1}, // bottom_left + {-1, 1, 1}, // bottom_right + {1, 1, 1}, // top_right + }; + + osg::Vec4 colors[]{ + osg::Vec4(1.0f, 0.0f, 0.0f, 0.0f), + osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), + osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), + osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), + osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f), + }; + + const int origin = 0; + const int top_left = 1; + const int bottom_left = 2; + const int bottom_right = 3; + const int top_right = 4; + + const int triangles[] = + { + bottom_right, top_right, top_left, + bottom_right, top_left, bottom_left, + origin, top_left, top_right, + origin, top_right, bottom_right, + origin, bottom_left, top_left, + origin, bottom_right, bottom_left, + }; + int numVertices = sizeof(triangles) / sizeof(*triangles); + osg::ref_ptr vertexArray = new osg::Vec3Array(numVertices); + osg::ref_ptr colorArray = new osg::Vec4Array(numVertices); + for (int i = 0; i < numVertices; i++) + { + (*vertexArray)[i] = vertices[triangles[i]]; + (*colorArray)[i] = colors[triangles[i]]; + } + + osg::ref_ptr normals = new osg::Vec3Array; + normals->push_back(osg::Vec3(0.0f, -1.0f, 0.0f)); + + geometry->setVertexArray(vertexArray); + geometry->setColorArray(colorArray, osg::Array::BIND_PER_VERTEX); + geometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, numVertices)); + geometry->setSupportsDisplayList(false); + geometry->setDataVariance(osg::Object::STATIC); + + auto stateset = geometry->getOrCreateStateSet(); + stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + + return geometry; + } + + float VRAnimation::getVelocity(const std::string& groupname) const + { + return 0.0f; + } + + osg::Vec3f VRAnimation::runAnimation(float timepassed) + { + return NpcAnimation::runAnimation(timepassed); + } + + void VRAnimation::addControllers() + { + NpcAnimation::addControllers(); + + for (int i = 0; i < 2; ++i) + { + auto forearm = mNodeMap.find(i == 0 ? "bip01 l forearm" : "bip01 r forearm"); + if (forearm != mNodeMap.end()) + { + auto node = forearm->second; + node->removeUpdateCallback(mForearmControllers[i]); + node->addUpdateCallback(mForearmControllers[i]); + } + + auto hand = mNodeMap.find(i == 0 ? "bip01 l hand" : "bip01 r hand"); + if (hand != mNodeMap.end()) + { + auto node = hand->second; + node->removeUpdateCallback(mHandControllers[i]); + node->addUpdateCallback(mHandControllers[i]); + } + } + + auto hand = mNodeMap.find("bip01 r hand"); if (hand != mNodeMap.end()) { - auto node = hand->second; - node->removeUpdateCallback(mHandControllers[i]); - node->addUpdateCallback(mHandControllers[i]); + hand->second->removeChild(mWeaponDirectionTransform); + hand->second->addChild(mWeaponDirectionTransform); + } + auto finger = mNodeMap.find("bip01 r finger11"); + if (finger != mNodeMap.end()) + { + finger->second->removeChild(mPointerTransform); + finger->second->addChild(mPointerTransform); + } + + auto parent = mObjectRoot->getParent(0); + if (parent->getName() == "Player Root") + { + auto group = parent->asGroup(); + group->removeChildren(0, parent->getNumChildren()); + group->addChild(mModelOffset); + mModelOffset->addChild(mObjectRoot); + } + } + void VRAnimation::enableHeadAnimation(bool) + { + NpcAnimation::enableHeadAnimation(false); + } + void VRAnimation::setAccurateAiming(bool) + { + NpcAnimation::setAccurateAiming(false); + } + + bool VRAnimation::canPlaceObject() + { + const float maxDist = 200.f; + if (mPointerTarget.mHit) + { + // check if the wanted position is on a flat surface, and not e.g. against a vertical wall + if (std::acos((mPointerTarget.mHitNormalWorld / mPointerTarget.mHitNormalWorld.length()) * osg::Vec3f(0, 0, 1)) >= osg::DegreesToRadians(30.f)) + return false; + + return true; + } + + return false; + } + + const MWRender::RayResult& VRAnimation::getPointerTarget() const + { + return mPointerTarget; + } + + + MWWorld::Ptr VRAnimation::getTarget(const std::string& directorNode) + { + auto node = mNodeMap.find(directorNode); + auto* world = MWBase::Environment::get().getWorld(); + MWRender::RayResult result{}; + if (node != mNodeMap.end()) + if (world) + world->getTargetObject(result, node->second); + return result.mHitObject; + } + + osg::Matrix VRAnimation::getWeaponTransformMatrix() const + { + return osg::computeLocalToWorld(mWeaponDirectionTransform->getParentalNodePaths()[0]); + } + + void VRAnimation::updatePointerTarget() + { + auto* world = MWBase::Environment::get().getWorld(); + if (world) + { + mPointerRescale->setMatrix(osg::Matrix::scale(1, 1, 1)); + mDistanceToPointerTarget = world->getTargetObject(mPointerTarget, mPointerTransform); + + if (mDistanceToPointerTarget >= 0) + mPointerRescale->setMatrix(osg::Matrix::scale(0.25f, mDistanceToPointerTarget, 0.25f)); + else + mPointerRescale->setMatrix(osg::Matrix::scale(0.25f, 10000.f, 0.25f)); } } - auto hand = mNodeMap.find("bip01 r hand"); - if (hand != mNodeMap.end()) - { - hand->second->removeChild(mWeaponDirectionTransform); - hand->second->addChild(mWeaponDirectionTransform); - } - auto finger = mNodeMap.find("bip01 r finger11"); - if (finger != mNodeMap.end()) - { - finger->second->removeChild(mPointerTransform); - finger->second->addChild(mPointerTransform); - } - - auto parent = mObjectRoot->getParent(0); - if (parent->getName() == "Player Root") - { - auto group = parent->asGroup(); - group->removeChildren(0, parent->getNumChildren()); - group->addChild(mModelOffset); - mModelOffset->addChild(mObjectRoot); - } - - //auto wb = mNodeMap.find("weapon bone"); - //if (wb != mNodeMap.end()) - //{ - // wb->second->removeChild(mWeaponPointerTransform); - // wb->second->addChild(mWeaponPointerTransform); - //} -} -void VRAnimation::enableHeadAnimation(bool) -{ - NpcAnimation::enableHeadAnimation(false); -} -void VRAnimation::setAccurateAiming(bool) -{ - NpcAnimation::setAccurateAiming(false); -} - -bool VRAnimation::canPlaceObject() -{ - const float maxDist = 200.f; - if (mPointerTarget.mHit) - { - // check if the wanted position is on a flat surface, and not e.g. against a vertical wall - if (std::acos((mPointerTarget.mHitNormalWorld / mPointerTarget.mHitNormalWorld.length()) * osg::Vec3f(0, 0, 1)) >= osg::DegreesToRadians(30.f)) - return false; - - return true; - } - - return false; -} - -const MWRender::RayResult& VRAnimation::getPointerTarget() const -{ - return mPointerTarget; -} - - -MWWorld::Ptr VRAnimation::getTarget(const std::string& directorNode) -{ - auto node = mNodeMap.find(directorNode); - auto* world = MWBase::Environment::get().getWorld(); - MWRender::RayResult result{}; - if (node != mNodeMap.end()) - if (world) - world->getTargetObject(result, node->second); - return result.mHitObject; -} - -osg::Matrix VRAnimation::getWeaponTransformMatrix() const -{ - return osg::computeLocalToWorld(mWeaponDirectionTransform->getParentalNodePaths()[0]); -} - -void VRAnimation::updatePointerTarget() -{ - auto* world = MWBase::Environment::get().getWorld(); - if (world) - { - mPointerRescale->setMatrix(osg::Matrix::scale(1, 1, 1)); - mDistanceToPointerTarget = world->getTargetObject(mPointerTarget, mPointerTransform); - - if(mDistanceToPointerTarget >= 0) - mPointerRescale->setMatrix(osg::Matrix::scale(0.25f, mDistanceToPointerTarget, 0.25f)); - else - mPointerRescale->setMatrix(osg::Matrix::scale(0.25f, 10000.f, 0.25f)); - } -} - } diff --git a/apps/openmw/mwvr/vranimation.hpp b/apps/openmw/mwvr/vranimation.hpp index a0cde5666..fb595e62a 100644 --- a/apps/openmw/mwvr/vranimation.hpp +++ b/apps/openmw/mwvr/vranimation.hpp @@ -9,86 +9,91 @@ namespace MWVR { -class HandController; -class FingerController; -class ForearmController; + class HandController; + class FingerController; + class ForearmController; -/// Subclassing NpcAnimation to override behaviours not compatible with VR -class VRAnimation : public MWRender::NpcAnimation -{ -protected: - virtual void addControllers(); + /// Subclassing NpcAnimation to override behaviours not compatible with VR + class VRAnimation : public MWRender::NpcAnimation + { + protected: + virtual void addControllers(); -public: - /** - * @param ptr - * @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports - * one listener at a time, so you shouldn't do this if creating several NpcAnimations - * for the same Ptr, eg preview dolls for the player. - * Those need to be manually rendered anyway. - * @param disableSounds Same as \a disableListener but for playing items sounds - * @param xrSession The XR session that shall be used to track limbs - */ - VRAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, - bool disableSounds, std::shared_ptr xrSession ); - virtual ~VRAnimation(); + public: + /** + * @param ptr + * @param disableListener Don't listen for equipment changes and magic effects. InventoryStore only supports + * one listener at a time, so you shouldn't do this if creating several NpcAnimations + * for the same Ptr, eg preview dolls for the player. + * Those need to be manually rendered anyway. + * @param disableSounds Same as \a disableListener but for playing items sounds + * @param xrSession The XR session that shall be used to track limbs + */ + VRAnimation(const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, + bool disableSounds, std::shared_ptr xrSession); + virtual ~VRAnimation(); - /// Overridden to always be false - virtual void enableHeadAnimation(bool enable); + /// Overridden to always be false + virtual void enableHeadAnimation(bool enable); - /// Overridden to always be false - virtual void setAccurateAiming(bool enabled); + /// Overridden to always be false + virtual void setAccurateAiming(bool enabled); - /// Overridden, implementation tbd - virtual osg::Vec3f runAnimation(float timepassed); + /// Overridden, implementation tbd + virtual osg::Vec3f runAnimation(float timepassed); - /// A relative factor (0-1) that decides if and how much the skeleton should be pitched - /// to indicate the facing orientation of the character. - virtual void setPitchFactor(float factor) { mPitchFactor = factor; } + /// A relative factor (0-1) that decides if and how much the skeleton should be pitched + /// to indicate the facing orientation of the character. + virtual void setPitchFactor(float factor) { mPitchFactor = factor; } - /// Overriden to always be a variant of VM_VR* - virtual void setViewMode(ViewMode viewMode); + /// Overriden to always be a variant of VM_VR* + virtual void setViewMode(ViewMode viewMode); - /// Overriden to include VR modifications - virtual void updateParts(); + /// Overriden to include VR modifications + virtual void updateParts(); - /// Overrides finger animations to point forward - void setFingerPointingMode(bool enabled); - bool fingerPointingMode() const { return mFingerPointingMode; } + /// Overrides finger animations to point forward + void setFingerPointingMode(bool enabled); - bool canPlaceObject(); - ///< @return true if it is possible to place on object where the player is currently pointing + /// @return Whether animation is currently in finger pointing mode + bool fingerPointingMode() const { return mFingerPointingMode; } - const MWRender::RayResult& getPointerTarget() const; - ///< @return pointer to the object the player's melee weapon is currently intersecting. + /// @return true if it is possible to place on object where the player is currently pointing + bool canPlaceObject(); - void updatePointerTarget(); + /// @return pointer to the object the player's melee weapon is currently intersecting. + const MWRender::RayResult& getPointerTarget() const; - MWWorld::Ptr getTarget(const std::string& directorNode); + /// Update what object this vr animation is currently pointing at. + void updatePointerTarget(); - osg::Matrix getWeaponTransformMatrix() const; + /// @return whatever ref is the current pointer target, if any + MWWorld::Ptr getTarget(const std::string& directorNode); -protected: - static osg::ref_ptr createPointerGeometry(void); + /// @return world transform that yields the position and orientation of the current weapon + osg::Matrix getWeaponTransformMatrix() const; - float getVelocity(const std::string& groupname) const override; + protected: + static osg::ref_ptr createPointerGeometry(void); -protected: - std::shared_ptr mSession; - osg::ref_ptr mForearmControllers[2]; - osg::ref_ptr mHandControllers[2]; - osg::ref_ptr mIndexFingerControllers[2]; - osg::ref_ptr mModelOffset; + float getVelocity(const std::string& groupname) const override; - bool mFingerPointingMode{ false }; - osg::ref_ptr mPointerGeometry{ nullptr }; - osg::ref_ptr mPointerRescale{ nullptr }; - osg::ref_ptr mPointerTransform{ nullptr }; - osg::ref_ptr mWeaponDirectionTransform{ nullptr }; - osg::ref_ptr mWeaponPointerTransform{ nullptr }; - MWRender::RayResult mPointerTarget{}; - float mDistanceToPointerTarget{ -1.f }; -}; + protected: + std::shared_ptr mSession; + osg::ref_ptr mForearmControllers[2]; + osg::ref_ptr mHandControllers[2]; + osg::ref_ptr mIndexFingerControllers[2]; + osg::ref_ptr mModelOffset; + + bool mFingerPointingMode{ false }; + osg::ref_ptr mPointerGeometry{ nullptr }; + osg::ref_ptr mPointerRescale{ nullptr }; + osg::ref_ptr mPointerTransform{ nullptr }; + osg::ref_ptr mWeaponDirectionTransform{ nullptr }; + osg::ref_ptr mWeaponPointerTransform{ nullptr }; + MWRender::RayResult mPointerTarget{}; + float mDistanceToPointerTarget{ -1.f }; + }; } diff --git a/apps/openmw/mwvr/vrenvironment.cpp b/apps/openmw/mwvr/vrenvironment.cpp index 5304122c2..43eb757e8 100644 --- a/apps/openmw/mwvr/vrenvironment.cpp +++ b/apps/openmw/mwvr/vrenvironment.cpp @@ -9,12 +9,12 @@ #include "../mwbase/environment.hpp" -MWVR::Environment *MWVR::Environment::sThis = 0; +MWVR::Environment* MWVR::Environment::sThis = 0; MWVR::Environment::Environment() -: mSession(nullptr) + : mSession(nullptr) { - assert (!sThis); + assert(!sThis); sThis = this; } @@ -42,15 +42,15 @@ void MWVR::Environment::cleanup() MWVR::Environment& MWVR::Environment::get() { - assert (sThis); + assert(sThis); return *sThis; } -MWVR::OpenXRInputManager* MWVR::Environment::getInputManager() const +MWVR::VRInputManager* MWVR::Environment::getInputManager() const { auto* inputManager = MWBase::Environment::get().getInputManager(); assert(inputManager); - auto xrInputManager = dynamic_cast(inputManager); + auto xrInputManager = dynamic_cast(inputManager); assert(xrInputManager); return xrInputManager; } diff --git a/apps/openmw/mwvr/vrenvironment.hpp b/apps/openmw/mwvr/vrenvironment.hpp index 196d16637..8aaec1058 100644 --- a/apps/openmw/mwvr/vrenvironment.hpp +++ b/apps/openmw/mwvr/vrenvironment.hpp @@ -4,15 +4,15 @@ namespace MWVR { class VRAnimation; - class OpenXRInputManager; + class VRInputManager; class VRSession; class VRGUIManager; class VRViewer; class OpenXRManager; - /// \brief Central hub for mw openxr subsystems + /// \brief Central hub for mw vr/openxr subsystems /// - /// This class allows each mw openxr subsystem to access any others subsystem's top-level manager class. + /// This class allows each mw subsystem to access any vr subsystem's top-level manager class. /// /// \attention Environment takes ownership of the manager class instances it is handed over in /// the set* functions. @@ -39,7 +39,7 @@ namespace MWVR static Environment& get(); ///< Return instance of this class. - MWVR::OpenXRInputManager* getInputManager() const; + MWVR::VRInputManager* getInputManager() const; // The OpenXRInputManager supplants the regular input manager // which is stored in MWBase::Environment diff --git a/apps/openmw/mwvr/vrgui.cpp b/apps/openmw/mwvr/vrgui.cpp index 8b73255e8..800b0333a 100644 --- a/apps/openmw/mwvr/vrgui.cpp +++ b/apps/openmw/mwvr/vrgui.cpp @@ -1,28 +1,35 @@ #include "vrgui.hpp" + #include "vrenvironment.hpp" #include "vrsession.hpp" #include "openxrmanagerimpl.hpp" #include "openxrinput.hpp" #include "vranimation.hpp" -#include + #include #include #include #include #include + +#include + #include #include #include -#include + #include "../mwrender/util.hpp" #include "../mwrender/renderbin.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/camera.hpp" #include "../mwrender/vismask.hpp" + #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" + #include "../mwgui/windowbase.hpp" + #include "../mwbase/statemanager.hpp" #include @@ -33,752 +40,752 @@ namespace osg { -// Convenience -const double PI_8 = osg::PI_4 / 2.; + // Convenience + const double PI_8 = osg::PI_4 / 2.; } namespace MWVR { -// When making a circle of a given radius of equally wide planes separated by a given angle, what is the width -static osg::Vec2 radiusAngleWidth(float radius, float angleRadian) -{ - const float width = std::fabs( 2.f * radius * std::tanf(angleRadian / 2.f) ); - return osg::Vec2(width, width); -} - -/// RTT camera used to draw the osg GUI to a texture -class GUICamera : public osg::Camera -{ -public: - GUICamera(int width, int height, osg::Vec4 clearColor) + // When making a circle of a given radius of equally wide planes separated by a given angle, what is the width + static osg::Vec2 radiusAngleWidth(float radius, float angleRadian) { - setRenderOrder(osg::Camera::PRE_RENDER); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setCullingActive(false); - - // Make the texture just a little transparent to feel more natural in the game world. - setClearColor(clearColor); - - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::ABSOLUTE_RF); - setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); - setName("GUICamera"); - - setCullMask(MWRender::Mask_GUI); - setNodeMask(MWRender::Mask_RenderToTexture); - - setViewport(0, 0, width, height); - - // No need for Update traversal since the mSceneRoot is already updated as part of the main scene graph - // A double update would mess with the light collection (in addition to being plain redundant) - setUpdateCallback(new MWRender::NoTraverseCallback); - - // Create the texture - mTexture = new osg::Texture2D; - mTexture->setTextureSize(width, height); - mTexture->setInternalFormat(GL_RGBA); - mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); - mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - attach(osg::Camera::COLOR_BUFFER, mTexture); - // Need to regenerate mipmaps every frame - setPostDrawCallback(new MWRender::MipmapCallback(mTexture)); - - // Do not want to waste time on shadows when generating the GUI texture - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); - - // Put rendering as early as possible - getOrCreateStateSet()->setRenderBinDetails(-1, "RenderBin"); - + const float width = std::fabs(2.f * radius * std::tanf(angleRadian / 2.f)); + return osg::Vec2(width, width); } - void setScene(osg::Node* scene) + /// RTT camera used to draw the osg GUI to a texture + class GUICamera : public osg::Camera { - if (mScene) - removeChild(mScene); - mScene = scene; - addChild(scene); - Log(Debug::Verbose) << "Set new scene: " << mScene->getName(); - } - - osg::Texture2D* getTexture() const - { - return mTexture.get(); - } - -private: - osg::ref_ptr mTexture; - osg::ref_ptr mScene; -}; - - -class LayerUpdateCallback : public osg::Callback -{ -public: - LayerUpdateCallback(VRGUILayer* layer) - : mLayer(layer) - { - - } - - bool run(osg::Object* object, osg::Object* data) - { - mLayer->update(); - return traverse(object, data); - } - -private: - VRGUILayer* mLayer; -}; - -VRGUILayer::VRGUILayer( - osg::ref_ptr geometryRoot, - osg::ref_ptr cameraRoot, - std::string layerName, - LayerConfig config, - VRGUIManager* parent) - : mConfig(config) - , mLayerName(layerName) - , mGeometryRoot(geometryRoot) - , mCameraRoot(cameraRoot) -{ - osg::ref_ptr vertices{ new osg::Vec3Array(4) }; - osg::ref_ptr texCoords{ new osg::Vec2Array(4) }; - osg::ref_ptr normals{ new osg::Vec3Array(1) }; - - auto extent_units = config.extent * Environment::get().unitsPerMeter(); - - float left = mConfig.center.x() - 0.5; - float right = left + 1.f; - float top = 0.5f + mConfig.center.y(); - float bottom = top - 1.f; - - // Define the menu quad - osg::Vec3 top_left (left, 1, top); - osg::Vec3 bottom_left(left, 1, bottom); - osg::Vec3 bottom_right(right , 1, bottom); - osg::Vec3 top_right (right, 1, top); - (*vertices)[0] = top_left; - (*vertices)[1] = bottom_left; - (*vertices)[2] = bottom_right; - (*vertices)[3] = top_right; - mGeometry->setVertexArray(vertices); - (*texCoords)[0].set(0.0f, 1.0f); - (*texCoords)[1].set(0.0f, 0.0f); - (*texCoords)[2].set(1.0f, 0.0f); - (*texCoords)[3].set(1.0f, 1.0f); - mGeometry->setTexCoordArray(0, texCoords); - (*normals)[0].set(0.0f, -1.0f, 0.0f); - mGeometry->setNormalArray(normals, osg::Array::BIND_OVERALL); - mGeometry->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4)); - mGeometry->setDataVariance(osg::Object::STATIC); - mGeometry->setSupportsDisplayList(false); - mGeometry->setName("VRGUILayer"); - - // Create the camera that will render the menu texture - std::string filter = mLayerName; - if (!mConfig.extraLayers.empty()) - filter = filter + ";" + mConfig.extraLayers; - mGUICamera = new GUICamera(config.pixelResolution.x(), config.pixelResolution.y(), config.backgroundColor); - osgMyGUI::RenderManager& renderManager = static_cast(MyGUI::RenderManager::getInstance()); - mMyGUICamera = renderManager.createGUICamera(osg::Camera::NESTED_RENDER, filter); - //myGUICamera->setViewport(0, 0, 256, 256); - //mMyGUICamera->setProjectionMatrixAsOrtho2D(-1, 1, -1, 1); - mGUICamera->setScene(mMyGUICamera); - - // Define state set that allows rendering with transparency - osg::StateSet* stateSet = mGeometry->getOrCreateStateSet(); - stateSet->setTextureAttributeAndModes(0, menuTexture(), osg::StateAttribute::ON); - stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); - stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); - stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); - stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); - mGeometry->setStateSet(stateSet); - - // Position in the game world - mTransform->setScale(osg::Vec3(extent_units.x(), 1.f, extent_units.y())); - mTransform->addChild(mGeometry); - - // Add to scene graph - mGeometryRoot->addChild(mTransform); - mCameraRoot->addChild(mGUICamera); - - // Edit offset to account for priority - if (!mConfig.sideBySide) - { - mConfig.offset.y() -= 0.001f * mConfig.priority; - } - - mTransform->addUpdateCallback(new LayerUpdateCallback(this)); -} - -VRGUILayer::~VRGUILayer() -{ - mGeometryRoot->removeChild(mTransform); - mCameraRoot->removeChild(mGUICamera); -} -osg::Camera* VRGUILayer::camera() -{ - return mGUICamera.get(); -} - -osg::ref_ptr VRGUILayer::menuTexture() -{ - if (mGUICamera) - return mGUICamera->getTexture(); - return nullptr; -} - -void VRGUILayer::setAngle(float angle) -{ - mRotation = osg::Quat{ angle, osg::Z_AXIS }; - updatePose(); -} - -void VRGUILayer::updateTracking(const Pose& headPose) -{ - if (mConfig.trackingMode == TrackingMode::Menu) - { - mTrackedPose = headPose; - } - else - { - auto* anim = MWVR::Environment::get().getPlayerAnimation(); - if (anim) + public: + GUICamera(int width, int height, osg::Vec4 clearColor) { - const osg::Node* hand = nullptr; - if (mConfig.trackingMode == TrackingMode::HudLeftHand) - hand = anim->getNode("bip01 l hand"); - else - hand = anim->getNode("bip01 r hand"); - if (hand) + setRenderOrder(osg::Camera::PRE_RENDER); + setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + setCullingActive(false); + + // Make the texture just a little transparent to feel more natural in the game world. + setClearColor(clearColor); + + setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + setReferenceFrame(osg::Camera::ABSOLUTE_RF); + setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); + setName("GUICamera"); + + setCullMask(MWRender::Mask_GUI); + setNodeMask(MWRender::Mask_RenderToTexture); + + setViewport(0, 0, width, height); + + // No need for Update traversal since the mSceneRoot is already updated as part of the main scene graph + // A double update would mess with the light collection (in addition to being plain redundant) + setUpdateCallback(new MWRender::NoTraverseCallback); + + // Create the texture + mTexture = new osg::Texture2D; + mTexture->setTextureSize(width, height); + mTexture->setInternalFormat(GL_RGBA); + mTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); + mTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + mTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + mTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + attach(osg::Camera::COLOR_BUFFER, mTexture); + // Need to regenerate mipmaps every frame + setPostDrawCallback(new MWRender::MipmapCallback(mTexture)); + + // Do not want to waste time on shadows when generating the GUI texture + SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + + // Put rendering as early as possible + getOrCreateStateSet()->setRenderBinDetails(-1, "RenderBin"); + + } + + void setScene(osg::Node* scene) + { + if (mScene) + removeChild(mScene); + mScene = scene; + addChild(scene); + Log(Debug::Verbose) << "Set new scene: " << mScene->getName(); + } + + osg::Texture2D* getTexture() const + { + return mTexture.get(); + } + + private: + osg::ref_ptr mTexture; + osg::ref_ptr mScene; + }; + + + class LayerUpdateCallback : public osg::Callback + { + public: + LayerUpdateCallback(VRGUILayer* layer) + : mLayer(layer) + { + + } + + bool run(osg::Object* object, osg::Object* data) + { + mLayer->update(); + return traverse(object, data); + } + + private: + VRGUILayer* mLayer; + }; + + VRGUILayer::VRGUILayer( + osg::ref_ptr geometryRoot, + osg::ref_ptr cameraRoot, + std::string layerName, + LayerConfig config, + VRGUIManager* parent) + : mConfig(config) + , mLayerName(layerName) + , mGeometryRoot(geometryRoot) + , mCameraRoot(cameraRoot) + { + osg::ref_ptr vertices{ new osg::Vec3Array(4) }; + osg::ref_ptr texCoords{ new osg::Vec2Array(4) }; + osg::ref_ptr normals{ new osg::Vec3Array(1) }; + + auto extent_units = config.extent * Environment::get().unitsPerMeter(); + + float left = mConfig.center.x() - 0.5; + float right = left + 1.f; + float top = 0.5f + mConfig.center.y(); + float bottom = top - 1.f; + + // Define the menu quad + osg::Vec3 top_left(left, 1, top); + osg::Vec3 bottom_left(left, 1, bottom); + osg::Vec3 bottom_right(right, 1, bottom); + osg::Vec3 top_right(right, 1, top); + (*vertices)[0] = top_left; + (*vertices)[1] = bottom_left; + (*vertices)[2] = bottom_right; + (*vertices)[3] = top_right; + mGeometry->setVertexArray(vertices); + (*texCoords)[0].set(0.0f, 1.0f); + (*texCoords)[1].set(0.0f, 0.0f); + (*texCoords)[2].set(1.0f, 0.0f); + (*texCoords)[3].set(1.0f, 1.0f); + mGeometry->setTexCoordArray(0, texCoords); + (*normals)[0].set(0.0f, -1.0f, 0.0f); + mGeometry->setNormalArray(normals, osg::Array::BIND_OVERALL); + mGeometry->addPrimitiveSet(new osg::DrawArrays(GL_QUADS, 0, 4)); + mGeometry->setDataVariance(osg::Object::STATIC); + mGeometry->setSupportsDisplayList(false); + mGeometry->setName("VRGUILayer"); + + // Create the camera that will render the menu texture + std::string filter = mLayerName; + if (!mConfig.extraLayers.empty()) + filter = filter + ";" + mConfig.extraLayers; + mGUICamera = new GUICamera(config.pixelResolution.x(), config.pixelResolution.y(), config.backgroundColor); + osgMyGUI::RenderManager& renderManager = static_cast(MyGUI::RenderManager::getInstance()); + mMyGUICamera = renderManager.createGUICamera(osg::Camera::NESTED_RENDER, filter); + //myGUICamera->setViewport(0, 0, 256, 256); + //mMyGUICamera->setProjectionMatrixAsOrtho2D(-1, 1, -1, 1); + mGUICamera->setScene(mMyGUICamera); + + // Define state set that allows rendering with transparency + osg::StateSet* stateSet = mGeometry->getOrCreateStateSet(); + stateSet->setTextureAttributeAndModes(0, menuTexture(), osg::StateAttribute::ON); + stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + stateSet->setMode(GL_BLEND, osg::StateAttribute::ON); + stateSet->setAttributeAndModes(new osg::BlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + stateSet->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); + mGeometry->setStateSet(stateSet); + + // Position in the game world + mTransform->setScale(osg::Vec3(extent_units.x(), 1.f, extent_units.y())); + mTransform->addChild(mGeometry); + + // Add to scene graph + mGeometryRoot->addChild(mTransform); + mCameraRoot->addChild(mGUICamera); + + // Edit offset to account for priority + if (!mConfig.sideBySide) + { + mConfig.offset.y() -= 0.001f * mConfig.priority; + } + + mTransform->addUpdateCallback(new LayerUpdateCallback(this)); + } + + VRGUILayer::~VRGUILayer() + { + mGeometryRoot->removeChild(mTransform); + mCameraRoot->removeChild(mGUICamera); + } + osg::Camera* VRGUILayer::camera() + { + return mGUICamera.get(); + } + + osg::ref_ptr VRGUILayer::menuTexture() + { + if (mGUICamera) + return mGUICamera->getTexture(); + return nullptr; + } + + void VRGUILayer::setAngle(float angle) + { + mRotation = osg::Quat{ angle, osg::Z_AXIS }; + updatePose(); + } + + void VRGUILayer::updateTracking(const Pose& headPose) + { + if (mConfig.trackingMode == TrackingMode::Menu) + { + mTrackedPose = headPose; + } + else + { + auto* anim = MWVR::Environment::get().getPlayerAnimation(); + if (anim) { - auto world = osg::computeLocalToWorld(hand->getParentalNodePaths()[0]); - mTrackedPose.position = world.getTrans(); - mTrackedPose.orientation = world.getRotate(); - if (mConfig.trackingMode == TrackingMode::HudRightHand) + const osg::Node* hand = nullptr; + if (mConfig.trackingMode == TrackingMode::HudLeftHand) + hand = anim->getNode("bip01 l hand"); + else + hand = anim->getNode("bip01 r hand"); + if (hand) + { + auto world = osg::computeLocalToWorld(hand->getParentalNodePaths()[0]); + mTrackedPose.position = world.getTrans(); + mTrackedPose.orientation = world.getRotate(); + if (mConfig.trackingMode == TrackingMode::HudRightHand) + mTrackedPose.orientation = osg::Quat(osg::PI, osg::Vec3(1, 0, 0)) * mTrackedPose.orientation; + mTrackedPose.orientation = osg::Quat(osg::PI_2, osg::Vec3(0, 0, 1)) * mTrackedPose.orientation; mTrackedPose.orientation = osg::Quat(osg::PI, osg::Vec3(1, 0, 0)) * mTrackedPose.orientation; - mTrackedPose.orientation = osg::Quat(osg::PI_2, osg::Vec3(0, 0, 1)) * mTrackedPose.orientation; - mTrackedPose.orientation = osg::Quat(osg::PI, osg::Vec3(1, 0, 0)) * mTrackedPose.orientation; + } + } + } + + updatePose(); + } + + void VRGUILayer::updatePose() + { + + auto orientation = mRotation * mTrackedPose.orientation; + + if (mConfig.trackingMode == TrackingMode::Menu) + { + // Force menu layers to be vertical + auto axis = osg::Z_AXIS; + osg::Quat vertical; + auto local = orientation * axis; + vertical.makeRotate(local, axis); + orientation = orientation * vertical; + } + // Orient the offset and move the layer + auto position = mTrackedPose.position + orientation * mConfig.offset * MWVR::Environment::get().unitsPerMeter(); + + mTransform->setAttitude(orientation); + mTransform->setPosition(position); + } + + void VRGUILayer::updateRect() + { + auto viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + mRealRect.left = 1.f; + mRealRect.top = 1.f; + mRealRect.right = 0.f; + mRealRect.bottom = 0.f; + float realWidth = static_cast(viewSize.width); + float realHeight = static_cast(viewSize.height); + for (auto* widget : mWidgets) + { + auto rect = widget->mMainWidget->getAbsoluteRect(); + mRealRect.left = std::min(static_cast(rect.left) / realWidth, mRealRect.left); + mRealRect.top = std::min(static_cast(rect.top) / realHeight, mRealRect.top); + mRealRect.right = std::max(static_cast(rect.right) / realWidth, mRealRect.right); + mRealRect.bottom = std::max(static_cast(rect.bottom) / realHeight, mRealRect.bottom); + } + + // Some widgets don't capture the full visual + if (mLayerName == "JournalBooks") + { + mRealRect.left = 0.f; + mRealRect.top = 0.f; + mRealRect.right = 1.f; + mRealRect.bottom = 1.f; + } + + if (mLayerName == "Notification") + { + // The latest widget for notification is always the top one + // So we just have to stretch the rectangle to the bottom + // TODO: This might get deprecated with this new system? + mRealRect.bottom = 1.f; + } + } + + void VRGUILayer::update() + { + if (mConfig.trackingMode != TrackingMode::Menu) + updateTracking(); + + if (mConfig.sideBySide) + { + // The side-by-side windows are also the resizable windows. + // Stretch according to config + // This genre of layer should only ever have 1 widget as it will cover the full layer + auto* widget = mWidgets.front(); + auto* myGUIWindow = dynamic_cast(widget->mMainWidget); + auto* windowBase = dynamic_cast(widget); + if (windowBase && myGUIWindow) + { + auto w = mConfig.myGUIViewSize.x(); + auto h = mConfig.myGUIViewSize.y(); + windowBase->setCoordf(0.f, 0.f, w, h); + windowBase->onWindowResize(myGUIWindow); + } + } + updateRect(); + + float w = 0.f; + float h = 0.f; + for (auto* widget : mWidgets) + { + w = std::max(w, (float)widget->mMainWidget->getWidth()); + h = std::max(h, (float)widget->mMainWidget->getHeight()); + } + + // Pixels per unit + float res = static_cast(mConfig.spatialResolution) / Environment::get().unitsPerMeter(); + + if (mConfig.sizingMode == SizingMode::Auto) + { + mTransform->setScale(osg::Vec3(w / res, 1.f, h / res)); + } + if (mLayerName == "Notification") + { + auto viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + h = (1.f - mRealRect.top) * viewSize.height; + mTransform->setScale(osg::Vec3(w / res, 1.f, h / res)); + } + + // Convert from [0,1] range to [-1,1] + float menuLeft = mRealRect.left * 2. - 1.; + float menuRight = mRealRect.right * 2. - 1.; + // Opposite convention + float menuBottom = (1.f - mRealRect.bottom) * 2. - 1.; + float menuTop = (1.f - mRealRect.top) * 2.f - 1.; + + mMyGUICamera->setProjectionMatrixAsOrtho2D(menuLeft, menuRight, menuBottom, menuTop); + } + + void + VRGUILayer::insertWidget( + MWGui::Layout* widget) + { + for (auto* w : mWidgets) + if (w == widget) + return; + mWidgets.push_back(widget); + } + + void + VRGUILayer::removeWidget( + MWGui::Layout* widget) + { + for (auto it = mWidgets.begin(); it != mWidgets.end(); it++) + { + if (*it == widget) + { + mWidgets.erase(it); + return; } } } - updatePose(); -} - -void VRGUILayer::updatePose() -{ - - auto orientation = mRotation * mTrackedPose.orientation; - - if (mConfig.trackingMode == TrackingMode::Menu) + VRGUIManager::VRGUIManager( + osg::ref_ptr viewer) + : mOsgViewer(viewer) { - // Force menu layers to be vertical - auto axis = osg::Z_AXIS; - osg::Quat vertical; - auto local = orientation * axis; - vertical.makeRotate(local, axis); - orientation = orientation * vertical; - } - // Orient the offset and move the layer - auto position = mTrackedPose.position + orientation * mConfig.offset * MWVR::Environment::get().unitsPerMeter(); + mGUIGeometriesRoot->setName("VR GUI Geometry Root"); + mGUICamerasRoot->setName("VR GUI Cameras Root"); + auto* root = viewer->getSceneData(); + root->asGroup()->addChild(mGUICamerasRoot); + root->asGroup()->addChild(mGUIGeometriesRoot); - mTransform->setAttitude(orientation); - mTransform->setPosition(position); -} - -void VRGUILayer::updateRect() -{ - auto viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - mRealRect.left = 1.f; - mRealRect.top = 1.f; - mRealRect.right = 0.f; - mRealRect.bottom = 0.f; - float realWidth = static_cast(viewSize.width); - float realHeight = static_cast(viewSize.height); - for (auto* widget : mWidgets) - { - auto rect = widget->mMainWidget->getAbsoluteRect(); - mRealRect.left = std::min(static_cast(rect.left) / realWidth, mRealRect.left); - mRealRect.top = std::min(static_cast(rect.top) / realHeight, mRealRect.top); - mRealRect.right = std::max(static_cast(rect.right) / realWidth, mRealRect.right); - mRealRect.bottom = std::max(static_cast(rect.bottom) / realHeight, mRealRect.bottom); } - // Some widgets don't capture the full visual - if (mLayerName == "JournalBooks" ) + VRGUIManager::~VRGUIManager(void) { - mRealRect.left = 0.f; - mRealRect.top = 0.f; - mRealRect.right = 1.f; - mRealRect.bottom = 1.f; } - if (mLayerName == "Notification") + static const LayerConfig createDefaultConfig(int priority, bool background = true, SizingMode sizingMode = SizingMode::Auto) { - // The latest widget for notification is always the top one - // So we just have to stretch the rectangle to the bottom - // TODO: This might get deprecated with this new system? - mRealRect.bottom = 1.f; + return LayerConfig{ + priority, + false, // side-by-side + background ? osg::Vec4{0.f,0.f,0.f,.75f} : osg::Vec4{}, // background + osg::Vec3(0.f,0.66f,-.25f), // offset + osg::Vec2(0.f,0.f), // center (model space) + osg::Vec2(1.f, 1.f), // extent (meters) + 1024, // Spatial resolution (pixels per meter) + osg::Vec2i(2048,2048), // Texture resolution + osg::Vec2(1,1), + sizingMode, + TrackingMode::Menu, + "Popup" + }; } -} + LayerConfig gDefaultConfig = createDefaultConfig(1); + LayerConfig gVideoPlayerConfig = createDefaultConfig(1, true, SizingMode::Fixed); + LayerConfig gLoadingScreenConfig = createDefaultConfig(1, true, SizingMode::Fixed); + LayerConfig gJournalBooksConfig = createDefaultConfig(2, false, SizingMode::Fixed); + LayerConfig gDefaultWindowsConfig = createDefaultConfig(3, true); + LayerConfig gMessageBoxConfig = createDefaultConfig(6, false, SizingMode::Auto);; + LayerConfig gNotificationConfig = createDefaultConfig(7, false, SizingMode::Fixed); -void VRGUILayer::update() -{ - if (mConfig.trackingMode != TrackingMode::Menu) - updateTracking(); - - if (mConfig.sideBySide) + static const float sSideBySideRadius = 1.f; + static const float sSideBySideAzimuthInterval = -osg::PI_4; + static const LayerConfig createSideBySideConfig(int priority) { - // The side-by-side windows are also the resizable windows. - // Stretch according to config - // This genre of layer should only ever have 1 widget as it will cover the full layer - auto* widget = mWidgets.front(); - auto* myGUIWindow = dynamic_cast(widget->mMainWidget); - auto* windowBase = dynamic_cast(widget); - if (windowBase && myGUIWindow) - { - auto w = mConfig.myGUIViewSize.x(); - auto h = mConfig.myGUIViewSize.y(); - windowBase->setCoordf(0.f, 0.f, w, h); - windowBase->onWindowResize(myGUIWindow); - } - } - updateRect(); - - float w = 0.f; - float h = 0.f; - for (auto* widget : mWidgets) - { - w = std::max(w, (float)widget->mMainWidget->getWidth()); - h = std::max(h, (float)widget->mMainWidget->getHeight()); - } - - // Pixels per unit - float res = static_cast(mConfig.spatialResolution) / Environment::get().unitsPerMeter(); - - if (mConfig.sizingMode == SizingMode::Auto) - { - mTransform->setScale(osg::Vec3(w / res, 1.f, h / res)); - } - if (mLayerName == "Notification") - { - auto viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - h = (1.f - mRealRect.top) * viewSize.height; - mTransform->setScale(osg::Vec3(w / res, 1.f, h / res)); - } - - // Convert from [0,1] range to [-1,1] - float menuLeft = mRealRect.left * 2. - 1.; - float menuRight = mRealRect.right * 2. - 1.; - // Opposite convention - float menuBottom = (1.f - mRealRect.bottom) * 2. - 1.; - float menuTop = (1.f - mRealRect.top) * 2.f - 1.; - - mMyGUICamera->setProjectionMatrixAsOrtho2D(menuLeft, menuRight, menuBottom, menuTop); -} - -void -VRGUILayer::insertWidget( - MWGui::Layout* widget) -{ - for (auto* w : mWidgets) - if (w == widget) - return; - mWidgets.push_back(widget); -} - -void -VRGUILayer::removeWidget( - MWGui::Layout* widget) -{ - for (auto it = mWidgets.begin(); it != mWidgets.end(); it++) - { - if (*it == widget) - { - mWidgets.erase(it); - return; - } - } -} - -VRGUIManager::VRGUIManager( - osg::ref_ptr viewer) - : mOsgViewer(viewer) -{ - mGUIGeometriesRoot->setName("VR GUI Geometry Root"); - mGUICamerasRoot->setName("VR GUI Cameras Root"); - auto* root = viewer->getSceneData(); - root->asGroup()->addChild(mGUICamerasRoot); - root->asGroup()->addChild(mGUIGeometriesRoot); - -} - -VRGUIManager::~VRGUIManager(void) -{ -} - -static const LayerConfig createDefaultConfig(int priority, bool background = true, SizingMode sizingMode = SizingMode::Auto) -{ - return LayerConfig{ - priority, - false, // side-by-side - background ? osg::Vec4{0.f,0.f,0.f,.75f} : osg::Vec4{}, // background - osg::Vec3(0.f,0.66f,-.25f), // offset - osg::Vec2(0.f,0.f), // center (model space) - osg::Vec2(1.f, 1.f), // extent (meters) - 1024, // Spatial resolution (pixels per meter) - osg::Vec2i(2048,2048), // Texture resolution - osg::Vec2(1,1), - sizingMode, - TrackingMode::Menu, - "Popup" + return LayerConfig{ + priority, + true, // side-by-side + gDefaultConfig.backgroundColor, + osg::Vec3(0.f,sSideBySideRadius,-.25f), // offset + gDefaultConfig.center, + radiusAngleWidth(sSideBySideRadius, sSideBySideAzimuthInterval), // extent (meters) + gDefaultConfig.spatialResolution, + gDefaultConfig.pixelResolution, + osg::Vec2(0.70f, 0.70f), + SizingMode::Fixed, + gDefaultConfig.trackingMode, + "" + }; }; -} -LayerConfig gDefaultConfig = createDefaultConfig(1); -LayerConfig gVideoPlayerConfig = createDefaultConfig(1, true, SizingMode::Fixed); -LayerConfig gLoadingScreenConfig = createDefaultConfig(1, true, SizingMode::Fixed); -LayerConfig gJournalBooksConfig = createDefaultConfig(2, false, SizingMode::Fixed); -LayerConfig gDefaultWindowsConfig = createDefaultConfig(3, true); -LayerConfig gMessageBoxConfig = createDefaultConfig(6, false, SizingMode::Auto);; -LayerConfig gNotificationConfig = createDefaultConfig(7, false, SizingMode::Fixed); -static const float sSideBySideRadius = 1.f; -static const float sSideBySideAzimuthInterval = -osg::PI_4; -static const LayerConfig createSideBySideConfig(int priority) -{ - return LayerConfig{ - priority, - true, // side-by-side - gDefaultConfig.backgroundColor, - osg::Vec3(0.f,sSideBySideRadius,-.25f), // offset - gDefaultConfig.center, - radiusAngleWidth(sSideBySideRadius, sSideBySideAzimuthInterval), // extent (meters) - gDefaultConfig.spatialResolution, - gDefaultConfig.pixelResolution, - osg::Vec2(0.70f, 0.70f), - SizingMode::Fixed, - gDefaultConfig.trackingMode, + LayerConfig gStatsWindowConfig = createSideBySideConfig(0); + LayerConfig gInventoryWindowConfig = createSideBySideConfig(1); + LayerConfig gSpellWindowConfig = createSideBySideConfig(2); + LayerConfig gMapWindowConfig = createSideBySideConfig(3); + LayerConfig gInventoryCompanionWindowConfig = createSideBySideConfig(4); + LayerConfig gDialogueWindowConfig = createSideBySideConfig(5); + + LayerConfig gStatusHUDConfig = LayerConfig + { + 0, + false, // side-by-side + osg::Vec4{}, // background + osg::Vec3(0.025f,.025f,.066f), // offset (meters) + osg::Vec2(0.f,0.5f), // center (model space) + osg::Vec2(.1f, .1f), // extent (meters) + 1024, // resolution (pixels per meter) + osg::Vec2i(1024,1024), + gDefaultConfig.myGUIViewSize, + SizingMode::Auto, + TrackingMode::HudLeftHand, "" }; -}; -LayerConfig gStatsWindowConfig = createSideBySideConfig(0); -LayerConfig gInventoryWindowConfig = createSideBySideConfig(1); -LayerConfig gSpellWindowConfig = createSideBySideConfig(2); -LayerConfig gMapWindowConfig = createSideBySideConfig(3); -LayerConfig gInventoryCompanionWindowConfig = createSideBySideConfig(4); -LayerConfig gDialogueWindowConfig = createSideBySideConfig(5); - -LayerConfig gStatusHUDConfig = LayerConfig -{ - 0, - false, // side-by-side - osg::Vec4{}, // background - osg::Vec3(0.025f,.025f,.066f), // offset (meters) - osg::Vec2(0.f,0.5f), // center (model space) - osg::Vec2(.1f, .1f), // extent (meters) - 1024, // resolution (pixels per meter) - osg::Vec2i(1024,1024), - gDefaultConfig.myGUIViewSize, - SizingMode::Auto, - TrackingMode::HudLeftHand, - "" -}; - -LayerConfig gPopupConfig = LayerConfig -{ - 0, - false, // side-by-side - osg::Vec4{0.f,0.f,0.f,0.f}, // background - osg::Vec3(-0.025f,.025f,.066f), // offset (meters) - osg::Vec2(0.f,0.5f), // center (model space) - osg::Vec2(.1f, .1f), // extent (meters) - 1024, // resolution (pixels per meter) - osg::Vec2i(2048,2048), - gDefaultConfig.myGUIViewSize, - SizingMode::Auto, - TrackingMode::HudRightHand, - "" -}; - - - -static std::map gLayerConfigs = -{ - {"StatusHUD", gStatusHUDConfig}, - {"Tooltip", gPopupConfig}, - {"JournalBooks", gJournalBooksConfig}, - {"InventoryCompanionWindow", gInventoryCompanionWindowConfig}, - {"InventoryWindow", gInventoryWindowConfig}, - {"SpellWindow", gSpellWindowConfig}, - {"MapWindow", gMapWindowConfig}, - {"StatsWindow", gStatsWindowConfig}, - {"DialogueWindow", gDialogueWindowConfig}, - {"MessageBox", gMessageBoxConfig}, - {"Windows", gDefaultWindowsConfig}, - {"Notification", gNotificationConfig}, - {"VideoPlayer", gVideoPlayerConfig}, - {"LoadingScreen", gLoadingScreenConfig}, -}; - -static std::set layerBlacklist = -{ - "Overlay", - "AdditiveOverlay", -}; - -void VRGUIManager::updateSideBySideLayers() -{ - // Nothing to update - if (mSideBySideLayers.size() == 0) - return; - - std::sort(mSideBySideLayers.begin(), mSideBySideLayers.end(), [](const auto& lhs, const auto& rhs) { return *lhs < *rhs; }); - - int n = mSideBySideLayers.size(); - - float span = sSideBySideAzimuthInterval * (n - 1); // zero index, places lone layers straight ahead - float low = -span / 2; - - for (unsigned i = 0; i < mSideBySideLayers.size(); i++) - mSideBySideLayers[i]->setAngle(low + static_cast(i) * sSideBySideAzimuthInterval); -} - -void VRGUIManager::insertLayer(const std::string& name) -{ - LayerConfig config = gDefaultConfig; - auto configIt = gLayerConfigs.find(name); - if (configIt != gLayerConfigs.end()) + LayerConfig gPopupConfig = LayerConfig { - config = configIt->second; - } - else + 0, + false, // side-by-side + osg::Vec4{0.f,0.f,0.f,0.f}, // background + osg::Vec3(-0.025f,.025f,.066f), // offset (meters) + osg::Vec2(0.f,0.5f), // center (model space) + osg::Vec2(.1f, .1f), // extent (meters) + 1024, // resolution (pixels per meter) + osg::Vec2i(2048,2048), + gDefaultConfig.myGUIViewSize, + SizingMode::Auto, + TrackingMode::HudRightHand, + "" + }; + + + + static std::map gLayerConfigs = { - Log(Debug::Warning) << "Layer " << name << " has no configuration, using default"; - } + {"StatusHUD", gStatusHUDConfig}, + {"Tooltip", gPopupConfig}, + {"JournalBooks", gJournalBooksConfig}, + {"InventoryCompanionWindow", gInventoryCompanionWindowConfig}, + {"InventoryWindow", gInventoryWindowConfig}, + {"SpellWindow", gSpellWindowConfig}, + {"MapWindow", gMapWindowConfig}, + {"StatsWindow", gStatsWindowConfig}, + {"DialogueWindow", gDialogueWindowConfig}, + {"MessageBox", gMessageBoxConfig}, + {"Windows", gDefaultWindowsConfig}, + {"Notification", gNotificationConfig}, + {"VideoPlayer", gVideoPlayerConfig}, + {"LoadingScreen", gLoadingScreenConfig}, + }; - auto layer = std::shared_ptr(new VRGUILayer( - mGUIGeometriesRoot, - mGUICamerasRoot, - name, - config, - this - )); - mLayers[name] = layer; - - layer->mGeometry->setUserData(new VRGUILayerUserData(mLayers[name])); - - if (config.sideBySide) + static std::set layerBlacklist = { - mSideBySideLayers.push_back(layer); - updateSideBySideLayers(); - } + "Overlay", + "AdditiveOverlay", + }; - if (config.trackingMode == TrackingMode::Menu) - layer->updateTracking(mHeadPose); -} - -void VRGUIManager::insertWidget(MWGui::Layout* widget) -{ - auto* layer = widget->mMainWidget->getLayer(); - auto name = layer->getName(); - - auto it = mLayers.find(name); - if (it == mLayers.end()) + void VRGUIManager::updateSideBySideLayers() { - insertLayer(name); - it = mLayers.find(name); - if (it == mLayers.end()) - { - Log(Debug::Error) << "Failed to insert layer " << name; + // Nothing to update + if (mSideBySideLayers.size() == 0) return; - } + + std::sort(mSideBySideLayers.begin(), mSideBySideLayers.end(), [](const auto& lhs, const auto& rhs) { return *lhs < *rhs; }); + + int n = mSideBySideLayers.size(); + + float span = sSideBySideAzimuthInterval * (n - 1); // zero index, places lone layers straight ahead + float low = -span / 2; + + for (unsigned i = 0; i < mSideBySideLayers.size(); i++) + mSideBySideLayers[i]->setAngle(low + static_cast(i) * sSideBySideAzimuthInterval); } - it->second->insertWidget(widget); - - if (it->second.get() != mFocusLayer) - widget->setLayerPick(false); -} - -void VRGUIManager::removeLayer(const std::string& name) -{ - auto it = mLayers.find(name); - if (it == mLayers.end()) - return; - - auto layer = it->second; - - for (auto it2 = mSideBySideLayers.begin(); it2 < mSideBySideLayers.end(); it2++) + void VRGUIManager::insertLayer(const std::string& name) { - if (*it2 == layer) + LayerConfig config = gDefaultConfig; + auto configIt = gLayerConfigs.find(name); + if (configIt != gLayerConfigs.end()) { - mSideBySideLayers.erase(it2); + config = configIt->second; + } + else + { + Log(Debug::Warning) << "Layer " << name << " has no configuration, using default"; + } + + auto layer = std::shared_ptr(new VRGUILayer( + mGUIGeometriesRoot, + mGUICamerasRoot, + name, + config, + this + )); + mLayers[name] = layer; + + layer->mGeometry->setUserData(new VRGUILayerUserData(mLayers[name])); + + if (config.sideBySide) + { + mSideBySideLayers.push_back(layer); updateSideBySideLayers(); } + + if (config.trackingMode == TrackingMode::Menu) + layer->updateTracking(mHeadPose); } - if (it->second.get() == mFocusLayer) - setFocusLayer(nullptr); - - mLayers.erase(it); -} - -void VRGUIManager::removeWidget(MWGui::Layout* widget) -{ - auto* layer = widget->mMainWidget->getLayer(); - auto name = layer->getName(); - - auto it = mLayers.find(name); - if (it == mLayers.end()) + void VRGUIManager::insertWidget(MWGui::Layout* widget) { - //Log(Debug::Warning) << "Tried to remove widget from nonexistent layer " << name; - return; - } + auto* layer = widget->mMainWidget->getLayer(); + auto name = layer->getName(); - it->second->removeWidget(widget); - if (it->second->widgetCount() == 0) - { - removeLayer(name); - } -} - -void VRGUIManager::setVisible(MWGui::Layout* widget, bool visible) -{ - auto* layer = widget->mMainWidget->getLayer(); - auto name = layer->getName(); - - Log(Debug::Verbose) << "setVisible (" << name << "): " << visible; - if (layerBlacklist.find(name) != layerBlacklist.end()) - { - Log(Debug::Verbose) << "Blacklisted"; - // Never pick an invisible layer - widget->setLayerPick(false); - return; - } - - if (visible) - insertWidget(widget); - else - removeWidget(widget); -} - -void VRGUIManager::updateTracking(void) -{ - auto* world = MWBase::Environment::get().getWorld(); - if (!world) - return; - auto* camera = world->getRenderingManager().getCamera()->getOsgCamera(); - if (!camera) - return; - updateTracking(camera); -} - -void VRGUIManager::updateTracking(osg::Camera* camera) -{ - // Get head pose by reading the camera view matrix to place the GUI in the world. - osg::Vec3 eye{}; - osg::Vec3 center{}; - osg::Vec3 up{}; - Pose headPose{}; - - if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) - { - // If App is not running, tracking will not be propagated to camera. - // So we adopt stage space. - auto pose = MWVR::Environment::get().getSession()->predictedPoses(MWVR::VRSession::FramePhase::Update).head; - osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); - osg::Quat orientation = pose.orientation; - headPose.position = position; - headPose.orientation = orientation; - } - else - { - auto viewMatrix = camera->getViewMatrix(); - viewMatrix.getLookAt(eye, center, up); - headPose.position = eye; - headPose.orientation = viewMatrix.getRotate(); - headPose.orientation = headPose.orientation.inverse(); - } - - mHeadPose = headPose; - - for (auto& layer : mLayers) - layer.second->updateTracking(mHeadPose); -} - -bool VRGUIManager::updateFocus() -{ - auto* anim = MWVR::Environment::get().getPlayerAnimation(); - if (anim && anim->getPointerTarget().mHit) - { - std::shared_ptr newFocusLayer = nullptr; - auto* node = anim->getPointerTarget().mHitNode; - if (node->getName() == "VRGUILayer") + auto it = mLayers.find(name); + if (it == mLayers.end()) { - VRGUILayerUserData* userData = static_cast(node->getUserData()); - newFocusLayer = userData->mLayer.lock(); + insertLayer(name); + it = mLayers.find(name); + if (it == mLayers.end()) + { + Log(Debug::Error) << "Failed to insert layer " << name; + return; + } } - if (newFocusLayer && newFocusLayer->mLayerName != "Notification") + it->second->insertWidget(widget); + + if (it->second.get() != mFocusLayer) + widget->setLayerPick(false); + } + + void VRGUIManager::removeLayer(const std::string& name) + { + auto it = mLayers.find(name); + if (it == mLayers.end()) + return; + + auto layer = it->second; + + for (auto it2 = mSideBySideLayers.begin(); it2 < mSideBySideLayers.end(); it2++) { - setFocusLayer(newFocusLayer.get()); - computeGuiCursor(anim->getPointerTarget().mHitPointLocal); - return true; + if (*it2 == layer) + { + mSideBySideLayers.erase(it2); + updateSideBySideLayers(); + } + } + + if (it->second.get() == mFocusLayer) + setFocusLayer(nullptr); + + mLayers.erase(it); + } + + void VRGUIManager::removeWidget(MWGui::Layout* widget) + { + auto* layer = widget->mMainWidget->getLayer(); + auto name = layer->getName(); + + auto it = mLayers.find(name); + if (it == mLayers.end()) + { + //Log(Debug::Warning) << "Tried to remove widget from nonexistent layer " << name; + return; + } + + it->second->removeWidget(widget); + if (it->second->widgetCount() == 0) + { + removeLayer(name); } } - return false; -} -void VRGUIManager::setFocusLayer(VRGUILayer* layer) -{ - if (layer == mFocusLayer) - return; + void VRGUIManager::setVisible(MWGui::Layout* widget, bool visible) + { + auto* layer = widget->mMainWidget->getLayer(); + auto name = layer->getName(); - if (mFocusLayer) - { - mFocusLayer->mWidgets.front()->setLayerPick(false); - } - mFocusLayer = layer; - if (mFocusLayer) - { - Log(Debug::Verbose) << "Set focus layer to " << mFocusLayer->mWidgets.front()->mMainWidget->getLayer()->getName(); - mFocusLayer->mWidgets.front()->setLayerPick(true); - } - else - { - Log(Debug::Verbose) << "Set focus layer to null"; - } -} + Log(Debug::Verbose) << "setVisible (" << name << "): " << visible; + if (layerBlacklist.find(name) != layerBlacklist.end()) + { + Log(Debug::Verbose) << "Blacklisted"; + // Never pick an invisible layer + widget->setLayerPick(false); + return; + } -void VRGUIManager::computeGuiCursor(osg::Vec3 hitPoint) -{ - float x = 0; - float y = 0; - if (mFocusLayer) - { - osg::Vec2 bottomLeft = mFocusLayer->mConfig.center - osg::Vec2(0.5f, 0.5f); - x = hitPoint.x() - bottomLeft.x(); - y = hitPoint.z() - bottomLeft.y(); - auto rect = mFocusLayer->mRealRect; - auto viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - auto width = viewSize.width * rect.width(); - auto height = viewSize.height * rect.height(); - auto left = viewSize.width * rect.left; - auto bottom = viewSize.height * rect.bottom; - x = width * x + left; - y = bottom - height * y; + if (visible) + insertWidget(widget); + else + removeWidget(widget); } - mGuiCursor.x() = (int)x; - mGuiCursor.y() = (int)y; + void VRGUIManager::updateTracking(void) + { + auto* world = MWBase::Environment::get().getWorld(); + if (!world) + return; + auto* camera = world->getRenderingManager().getCamera()->getOsgCamera(); + if (!camera) + return; + updateTracking(camera); + } - MyGUI::InputManager::getInstance().injectMouseMove((int)x, (int)y, 0); - MWBase::Environment::get().getWindowManager()->setCursorActive(true); -} + void VRGUIManager::updateTracking(osg::Camera* camera) + { + // Get head pose by reading the camera view matrix to place the GUI in the world. + osg::Vec3 eye{}; + osg::Vec3 center{}; + osg::Vec3 up{}; + Pose headPose{}; + + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + { + // If App is not running, tracking will not be propagated to camera. + // So we adopt stage space. + auto pose = MWVR::Environment::get().getSession()->predictedPoses(MWVR::VRSession::FramePhase::Update).head; + osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); + osg::Quat orientation = pose.orientation; + headPose.position = position; + headPose.orientation = orientation; + } + else + { + auto viewMatrix = camera->getViewMatrix(); + viewMatrix.getLookAt(eye, center, up); + headPose.position = eye; + headPose.orientation = viewMatrix.getRotate(); + headPose.orientation = headPose.orientation.inverse(); + } + + mHeadPose = headPose; + + for (auto& layer : mLayers) + layer.second->updateTracking(mHeadPose); + } + + bool VRGUIManager::updateFocus() + { + auto* anim = MWVR::Environment::get().getPlayerAnimation(); + if (anim && anim->getPointerTarget().mHit) + { + std::shared_ptr newFocusLayer = nullptr; + auto* node = anim->getPointerTarget().mHitNode; + if (node->getName() == "VRGUILayer") + { + VRGUILayerUserData* userData = static_cast(node->getUserData()); + newFocusLayer = userData->mLayer.lock(); + } + + if (newFocusLayer && newFocusLayer->mLayerName != "Notification") + { + setFocusLayer(newFocusLayer.get()); + computeGuiCursor(anim->getPointerTarget().mHitPointLocal); + return true; + } + } + return false; + } + + void VRGUIManager::setFocusLayer(VRGUILayer* layer) + { + if (layer == mFocusLayer) + return; + + if (mFocusLayer) + { + mFocusLayer->mWidgets.front()->setLayerPick(false); + } + mFocusLayer = layer; + if (mFocusLayer) + { + Log(Debug::Verbose) << "Set focus layer to " << mFocusLayer->mWidgets.front()->mMainWidget->getLayer()->getName(); + mFocusLayer->mWidgets.front()->setLayerPick(true); + } + else + { + Log(Debug::Verbose) << "Set focus layer to null"; + } + } + + void VRGUIManager::computeGuiCursor(osg::Vec3 hitPoint) + { + float x = 0; + float y = 0; + if (mFocusLayer) + { + osg::Vec2 bottomLeft = mFocusLayer->mConfig.center - osg::Vec2(0.5f, 0.5f); + x = hitPoint.x() - bottomLeft.x(); + y = hitPoint.z() - bottomLeft.y(); + auto rect = mFocusLayer->mRealRect; + auto viewSize = MyGUI::RenderManager::getInstance().getViewSize(); + auto width = viewSize.width * rect.width(); + auto height = viewSize.height * rect.height(); + auto left = viewSize.width * rect.left; + auto bottom = viewSize.height * rect.bottom; + x = width * x + left; + y = bottom - height * y; + } + + mGuiCursor.x() = (int)x; + mGuiCursor.y() = (int)y; + + MyGUI::InputManager::getInstance().injectMouseMove((int)x, (int)y, 0); + MWBase::Environment::get().getWindowManager()->setCursorActive(true); + } } diff --git a/apps/openmw/mwvr/vrgui.hpp b/apps/openmw/mwvr/vrgui.hpp index 667be0242..92a89d678 100644 --- a/apps/openmw/mwvr/vrgui.hpp +++ b/apps/openmw/mwvr/vrgui.hpp @@ -17,14 +17,14 @@ namespace MyGUI { -class Widget; -class Window; + class Widget; + class Window; } namespace MWGui { -class Layout; -class WindowBase; + class Layout; + class WindowBase; } struct XrCompositionLayerQuad; diff --git a/apps/openmw/mwvr/vrinput.cpp b/apps/openmw/mwvr/vrinput.cpp index 8e38b21bf..8fe3636fd 100644 --- a/apps/openmw/mwvr/vrinput.cpp +++ b/apps/openmw/mwvr/vrinput.cpp @@ -10,134 +10,134 @@ namespace MWVR { -//! Delay before a long-press action is activated (and regular press is discarded) -//! TODO: Make this configurable? -static std::chrono::milliseconds gActionTime{ 666 }; -//! Magnitude above which an axis action is considered active -static float gAxisEpsilon{ 0.01f }; + //! Delay before a long-press action is activated (and regular press is discarded) + //! TODO: Make this configurable? + static std::chrono::milliseconds gActionTime{ 666 }; + //! Magnitude above which an axis action is considered active + static float gAxisEpsilon{ 0.01f }; -void HapticsAction::apply(float amplitude) -{ - mAmplitude = std::max(0.f, std::min(1.f, amplitude)); - mXRAction->applyHaptics(XR_NULL_PATH, mAmplitude); -} - -PoseAction::PoseAction(std::unique_ptr xrAction) - : mXRAction(std::move(xrAction)) - , mXRSpace{ XR_NULL_HANDLE } -{ - auto* xr = Environment::get().getManager(); - XrActionSpaceCreateInfo createInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO }; - createInfo.action = *mXRAction; - createInfo.poseInActionSpace.orientation.w = 1.f; - createInfo.subactionPath = XR_NULL_PATH; - CHECK_XRCMD(xrCreateActionSpace(xr->impl().xrSession(), &createInfo, &mXRSpace)); -} - -void PoseAction::update(long long time) -{ - mPrevious = mValue; - - auto* xr = Environment::get().getManager(); - XrSpace referenceSpace = xr->impl().getReferenceSpace(ReferenceSpace::STAGE); - - XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION }; - XrSpaceVelocity velocity{ XR_TYPE_SPACE_VELOCITY }; - location.next = &velocity; - CHECK_XRCMD(xrLocateSpace(mXRSpace, referenceSpace, time, &location)); - if (!(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) - // Quat must have a magnitude of 1 but openxr sets it to 0 when tracking is unavailable. - // I want a no-track pose to still be a valid quat so osg won't throw errors - location.pose.orientation.w = 1; - - mValue = Pose{ - fromXR(location.pose.position), - fromXR(location.pose.orientation) - }; -} - -void Action::updateAndQueue(std::deque& queue) -{ - bool old = mActive; - mPrevious = mValue; - update(); - bool changed = old != mActive; - mOnActivate = changed && mActive; - mOnDeactivate = changed && !mActive; - - if (shouldQueue()) + void HapticsAction::apply(float amplitude) { - queue.push_back(this); + mAmplitude = std::max(0.f, std::min(1.f, amplitude)); + mXRAction->applyHaptics(XR_NULL_PATH, mAmplitude); } -} -void ButtonPressAction::update() -{ - mActive = false; - bool old = mPressed; - mXRAction->getBool(0, mPressed); - bool changed = old != mPressed; - if (changed && mPressed) + PoseAction::PoseAction(std::unique_ptr xrAction) + : mXRAction(std::move(xrAction)) + , mXRSpace{ XR_NULL_HANDLE } { - mPressTime = std::chrono::steady_clock::now(); - mTimeout = mPressTime + gActionTime; + auto* xr = Environment::get().getManager(); + XrActionSpaceCreateInfo createInfo{ XR_TYPE_ACTION_SPACE_CREATE_INFO }; + createInfo.action = *mXRAction; + createInfo.poseInActionSpace.orientation.w = 1.f; + createInfo.subactionPath = XR_NULL_PATH; + CHECK_XRCMD(xrCreateActionSpace(xr->impl().xrSession(), &createInfo, &mXRSpace)); } - if (changed && !mPressed) + + void PoseAction::update(long long time) { - if (std::chrono::steady_clock::now() < mTimeout) + mPrevious = mValue; + + auto* xr = Environment::get().getManager(); + XrSpace referenceSpace = xr->impl().getReferenceSpace(ReferenceSpace::STAGE); + + XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION }; + XrSpaceVelocity velocity{ XR_TYPE_SPACE_VELOCITY }; + location.next = &velocity; + CHECK_XRCMD(xrLocateSpace(mXRSpace, referenceSpace, time, &location)); + if (!(location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) + // Quat must have a magnitude of 1 but openxr sets it to 0 when tracking is unavailable. + // I want a no-track pose to still be a valid quat so osg won't throw errors + location.pose.orientation.w = 1; + + mValue = Pose{ + fromXR(location.pose.position), + fromXR(location.pose.orientation) + }; + } + + void Action::updateAndQueue(std::deque& queue) + { + bool old = mActive; + mPrevious = mValue; + update(); + bool changed = old != mActive; + mOnActivate = changed && mActive; + mOnDeactivate = changed && !mActive; + + if (shouldQueue()) { + queue.push_back(this); + } + } + + void ButtonPressAction::update() + { + mActive = false; + bool old = mPressed; + mXRAction->getBool(0, mPressed); + bool changed = old != mPressed; + if (changed && mPressed) + { + mPressTime = std::chrono::steady_clock::now(); + mTimeout = mPressTime + gActionTime; + } + if (changed && !mPressed) + { + if (std::chrono::steady_clock::now() < mTimeout) + { + mActive = true; + } + } + + mValue = mPressed ? 1.f : 0.f; + } + + void ButtonLongPressAction::update() + { + mActive = false; + bool old = mPressed; + mXRAction->getBool(0, mPressed); + bool changed = old != mPressed; + if (changed && mPressed) + { + mPressTime = std::chrono::steady_clock::now(); + mTimein = mPressTime + gActionTime; + mActivated = false; + } + if (mPressed && !mActivated) + { + if (std::chrono::steady_clock::now() >= mTimein) + { + mActive = mActivated = true; + mValue = 1.f; + } + } + if (changed && !mPressed) + { + mValue = 0.f; + } + } + + + void ButtonHoldAction::update() + { + mXRAction->getBool(0, mPressed); + mActive = mPressed; + + mValue = mPressed ? 1.f : 0.f; + } + + + void AxisAction::update() + { + mActive = false; + mXRAction->getFloat(0, mValue); + + if (std::fabs(mValue) > gAxisEpsilon) mActive = true; - } + else + mValue = 0.f; } - mValue = mPressed ? 1.f : 0.f; -} - -void ButtonLongPressAction::update() -{ - mActive = false; - bool old = mPressed; - mXRAction->getBool(0, mPressed); - bool changed = old != mPressed; - if (changed && mPressed) - { - mPressTime = std::chrono::steady_clock::now(); - mTimein = mPressTime + gActionTime; - mActivated = false; - } - if (mPressed && !mActivated) - { - if (std::chrono::steady_clock::now() >= mTimein) - { - mActive = mActivated = true; - mValue = 1.f; - } - } - if (changed && !mPressed) - { - mValue = 0.f; - } -} - - -void ButtonHoldAction::update() -{ - mXRAction->getBool(0, mPressed); - mActive = mPressed; - - mValue = mPressed ? 1.f : 0.f; -} - - -void AxisAction::update() -{ - mActive = false; - mXRAction->getFloat(0, mValue); - - if (std::fabs(mValue) > gAxisEpsilon) - mActive = true; - else - mValue = 0.f; -} - } diff --git a/apps/openmw/mwvr/vrinput.hpp b/apps/openmw/mwvr/vrinput.hpp index 931158b9a..53f3c1f80 100644 --- a/apps/openmw/mwvr/vrinput.hpp +++ b/apps/openmw/mwvr/vrinput.hpp @@ -10,207 +10,218 @@ namespace MWVR { -// The OpenMW input manager iterates from 0 to A_Last in its actions enum. -// I don't know that it would cause any ill effects, but i nonetheless do not -// want to contaminate the input manager with my OpenXR specific actions. -// Therefore i add them here to a separate enum whose values start past A_Last. -enum VrActions -{ - A_VrFirst = MWInput::A_Last + 1, - A_ActivateTouch, - A_HapticsLeft, - A_HapticsRight, - A_HandPoseLeft, - A_HandPoseRight, - A_MenuUpDown, - A_MenuLeftRight, - A_MenuSelect, - A_MenuBack, - A_Recenter, - A_VrLast -}; + /// Extension of MWInput's set of actions. + enum VrActions + { + A_VrFirst = MWInput::A_Last + 1, + A_ActivateTouch, + A_HapticsLeft, + A_HapticsRight, + A_HandPoseLeft, + A_HandPoseRight, + A_MenuUpDown, + A_MenuLeftRight, + A_MenuSelect, + A_MenuBack, + A_Recenter, + A_VrLast + }; -enum class ActionPath -{ - Pose = 0, - Haptic, - Menu, - ThumbstickX, - ThumbstickY, - ThumbstickClick, - Select, - Squeeze, - Trigger, - X, - Y, - A, - B, - Last -}; + /// \brief Enum representation of action paths in openxr. + /// + /// OpenXR allows a lot of generics, consequentially this will most likely be changed/expanded + /// in the future as we need it. This set was added based on what the Oculus needed. + enum class ActionPath + { + Pose = 0, + Haptic, + Menu, + ThumbstickX, + ThumbstickY, + ThumbstickClick, + Select, + Squeeze, + Trigger, + X, + Y, + A, + B, + Last + }; -//class OpenXRAction; + /// \brief Suggest a binding by binding an action to a path on a given hand (left or right). + struct SuggestedBindings + { + struct Binding + { + int action; + ActionPath path; + Side side; + }; -//! Action for applying haptics -class HapticsAction -{ -public: - HapticsAction(std::unique_ptr xrAction) : mXRAction{ std::move(xrAction) } {}; + std::string controllerPath; + std::vector bindings; + }; - //! Apply vibration at the given amplitude - void apply(float amplitude); + /// \brief Action for applying haptics + class HapticsAction + { + public: + HapticsAction(std::unique_ptr xrAction) : mXRAction{ std::move(xrAction) } {}; - //! Convenience - operator XrAction() { return *mXRAction; } + //! Apply vibration at the given amplitude + void apply(float amplitude); -private: - std::unique_ptr mXRAction; - float mAmplitude{ 0.f }; -}; + //! Convenience + operator XrAction() { return *mXRAction; } -//! Action for capturing tracking information -class PoseAction -{ -public: - PoseAction(std::unique_ptr xrAction); + private: + std::unique_ptr mXRAction; + float mAmplitude{ 0.f }; + }; - //! Current value of an axis or lever action - Pose value() const { return mValue; } + /// \brief Action for capturing tracking information + class PoseAction + { + public: + PoseAction(std::unique_ptr xrAction); - //! Previous value - Pose previousValue() const { return mPrevious; } + //! Current value of an axis or lever action + Pose value() const { return mValue; } - //! Convenience - operator XrAction() { return *mXRAction; } + //! Previous value + Pose previousValue() const { return mPrevious; } - //! Update pose value - void update(long long time); + //! Convenience + operator XrAction() { return *mXRAction; } -private: - std::unique_ptr mXRAction; - XrSpace mXRSpace; - Pose mValue{}; - Pose mPrevious{}; -}; + //! Update pose value + void update(long long time); -//! Generic action -class Action -{ -public: - Action(int openMWAction, std::unique_ptr xrAction) : mXRAction(std::move(xrAction)), mOpenMWAction(openMWAction) {} - virtual ~Action() {}; + private: + std::unique_ptr mXRAction; + XrSpace mXRSpace; + Pose mValue{}; + Pose mPrevious{}; + }; - //! True if action changed to being released in the last update - bool isActive() const { return mActive; }; + /// \brief Generic action + /// \sa ButtonPressAction ButtonLongPressAction ButtonHoldAction AxisAction + class Action + { + public: + Action(int openMWAction, std::unique_ptr xrAction) : mXRAction(std::move(xrAction)), mOpenMWAction(openMWAction) {} + virtual ~Action() {}; - //! True if activation turned on this update (i.e. onPress) - bool onActivate() const { return mOnActivate; } + //! True if action changed to being released in the last update + bool isActive() const { return mActive; }; - //! True if activation turned off this update (i.e. onRelease) - bool onDeactivate() const { return mOnDeactivate; } + //! True if activation turned on this update (i.e. onPress) + bool onActivate() const { return mOnActivate; } - //! OpenMW Action code of this action - int openMWActionCode() const { return mOpenMWAction; } + //! True if activation turned off this update (i.e. onRelease) + bool onDeactivate() const { return mOnDeactivate; } - //! Current value of an axis or lever action - float value() const { return mValue; } + //! OpenMW Action code of this action + int openMWActionCode() const { return mOpenMWAction; } - //! Previous value - float previousValue() const { return mPrevious; } + //! Current value of an axis or lever action + float value() const { return mValue; } - //! Update internal states. Note that subclasses maintain both mValue and mActivate to allow - //! axis and press to subtitute one another. - virtual void update() = 0; + //! Previous value + float previousValue() const { return mPrevious; } - //! Determines if an action update should be queued - virtual bool shouldQueue() const = 0; + //! Update internal states. Note that subclasses maintain both mValue and mActivate to allow + //! axis and press to subtitute one another. + virtual void update() = 0; - //! Convenience - operator XrAction() { return *mXRAction; } + //! Determines if an action update should be queued + virtual bool shouldQueue() const = 0; - //! Update and queue action if applicable - void updateAndQueue(std::deque& queue); + //! Convenience + operator XrAction() { return *mXRAction; } -protected: + //! Update and queue action if applicable + void updateAndQueue(std::deque& queue); - std::unique_ptr mXRAction; - int mOpenMWAction; - float mValue{ 0.f }; - float mPrevious{ 0.f }; - bool mActive{ false }; - bool mOnActivate{ false }; - bool mOnDeactivate{ false }; -}; + protected: -//! Convenience -using ActionPtr = std::unique_ptr; + std::unique_ptr mXRAction; + int mOpenMWAction; + float mValue{ 0.f }; + float mPrevious{ 0.f }; + bool mActive{ false }; + bool mOnActivate{ false }; + bool mOnDeactivate{ false }; + }; -//! Action that activates once on release. -//! Times out if the button is held longer than gHoldDelay. -class ButtonPressAction : public Action -{ -public: - using Action::Action; + //! Action that activates once on release. + //! Times out if the button is held longer than gHoldDelay. + class ButtonPressAction : public Action + { + public: + using Action::Action; - static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; + static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; - void update() override; + void update() override; - virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); } + virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); } - bool mPressed{ false }; - std::chrono::steady_clock::time_point mPressTime{}; - std::chrono::steady_clock::time_point mTimeout{}; -}; + bool mPressed{ false }; + std::chrono::steady_clock::time_point mPressTime{}; + std::chrono::steady_clock::time_point mTimeout{}; + }; -//! Action that activates once on a long press -//! The press time is the same as the timeout for a regular press, allowing keys with double roles. -class ButtonLongPressAction : public Action -{ -public: - using Action::Action; + //! Action that activates once on a long press + //! The press time is the same as the timeout for a regular press, allowing keys with double roles. + class ButtonLongPressAction : public Action + { + public: + using Action::Action; - static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; + static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; - void update() override; + void update() override; - virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); } + virtual bool shouldQueue() const override { return onActivate() || onDeactivate(); } - bool mPressed{ false }; - bool mActivated{ false }; - std::chrono::steady_clock::time_point mPressTime{}; - std::chrono::steady_clock::time_point mTimein{}; -}; + bool mPressed{ false }; + bool mActivated{ false }; + std::chrono::steady_clock::time_point mPressTime{}; + std::chrono::steady_clock::time_point mTimein{}; + }; -//! Action that is active whenever the button is pressed down. -//! Useful for e.g. non-toggling sneak and automatically repeating actions -class ButtonHoldAction : public Action -{ -public: - using Action::Action; + //! Action that is active whenever the button is pressed down. + //! Useful for e.g. non-toggling sneak and automatically repeating actions + class ButtonHoldAction : public Action + { + public: + using Action::Action; - static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; + static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; - void update() override; + void update() override; - virtual bool shouldQueue() const override { return mActive || onDeactivate(); } + virtual bool shouldQueue() const override { return mActive || onDeactivate(); } - bool mPressed{ false }; -}; + bool mPressed{ false }; + }; -//! Action for axis actions, such as thumbstick axes or certain triggers/squeeze levers. -//! Float axis are considered active whenever their magnitude is greater than gAxisEpsilon. This is useful -//! as a touch subtitute on levers without touch. -class AxisAction : public Action -{ -public: - using Action::Action; + //! Action for axis actions, such as thumbstick axes or certain triggers/squeeze levers. + //! Float axis are considered active whenever their magnitude is greater than gAxisEpsilon. This is useful + //! as a touch subtitute on levers without touch. + class AxisAction : public Action + { + public: + using Action::Action; - static const XrActionType ActionType = XR_ACTION_TYPE_FLOAT_INPUT; + static const XrActionType ActionType = XR_ACTION_TYPE_FLOAT_INPUT; - void update() override; + void update() override; - virtual bool shouldQueue() const override { return mActive || onDeactivate(); } -}; + virtual bool shouldQueue() const override { return mActive || onDeactivate(); } + }; } #endif diff --git a/apps/openmw/mwvr/vrinputmanager.cpp b/apps/openmw/mwvr/vrinputmanager.cpp index 6110ed425..047857334 100644 --- a/apps/openmw/mwvr/vrinputmanager.cpp +++ b/apps/openmw/mwvr/vrinputmanager.cpp @@ -37,147 +37,147 @@ namespace MWVR { -Pose OpenXRInputManager::getLimbPose(int64_t time, TrackedLimb limb) -{ - return mXRInput->getLimbPose(time, limb); -} - -void OpenXRInputManager::updateActivationIndication(void) -{ - bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - bool show = guiMode | mActivationIndication; - auto* playerAnimation = Environment::get().getPlayerAnimation(); - if (playerAnimation) - playerAnimation->setFingerPointingMode(show); -} - - -/** - * Makes it possible to use ItemModel::moveItem to move an item from an inventory to the world. - */ -class DropItemAtPointModel : public MWGui::ItemModel -{ -public: - DropItemAtPointModel() {} - virtual ~DropItemAtPointModel() {} - virtual MWWorld::Ptr copyItem(const MWGui::ItemStack& item, size_t count, bool /*allowAutoEquip*/) + Pose VRInputManager::getLimbPose(int64_t time, TrackedLimb limb) { - MWBase::World* world = MWBase::Environment::get().getWorld(); - MWVR::VRAnimation* anim = MWVR::Environment::get().getPlayerAnimation(); + return mXRInput->getLimbPose(time, limb); + } - MWWorld::Ptr dropped; - if (anim->canPlaceObject()) - dropped = world->placeObject(item.mBase, anim->getPointerTarget(), count); + void VRInputManager::updateActivationIndication(void) + { + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + bool show = guiMode | mActivationIndication; + auto* playerAnimation = Environment::get().getPlayerAnimation(); + if (playerAnimation) + playerAnimation->setFingerPointingMode(show); + } + + + /** + * Makes it possible to use ItemModel::moveItem to move an item from an inventory to the world. + */ + class DropItemAtPointModel : public MWGui::ItemModel + { + public: + DropItemAtPointModel() {} + virtual ~DropItemAtPointModel() {} + virtual MWWorld::Ptr copyItem(const MWGui::ItemStack& item, size_t count, bool /*allowAutoEquip*/) + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + MWVR::VRAnimation* anim = MWVR::Environment::get().getPlayerAnimation(); + + MWWorld::Ptr dropped; + if (anim->canPlaceObject()) + dropped = world->placeObject(item.mBase, anim->getPointerTarget(), count); + else + dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count); + dropped.getCellRef().setOwner(""); + + return dropped; + } + + virtual void removeItem(const MWGui::ItemStack& item, size_t count) { throw std::runtime_error("removeItem not implemented"); } + virtual ModelIndex getIndex(MWGui::ItemStack item) { throw std::runtime_error("getIndex not implemented"); } + virtual void update() {} + virtual size_t getItemCount() { return 0; } + virtual MWGui::ItemStack getItem(ModelIndex index) { throw std::runtime_error("getItem not implemented"); } + + private: + // Where to drop the item + MWRender::RayResult mIntersection; + }; + + void VRInputManager::pointActivation(bool onPress) + { + auto* world = MWBase::Environment::get().getWorld(); + auto* anim = MWVR::Environment::get().getPlayerAnimation(); + if (world && anim && anim->getPointerTarget().mHit) + { + auto* node = anim->getPointerTarget().mHitNode; + MWWorld::Ptr ptr = anim->getPointerTarget().mHitObject; + auto& dnd = MWBase::Environment::get().getWindowManager()->getDragAndDrop(); + + if (node && node->getName() == "VRGUILayer") + { + injectMousePress(SDL_BUTTON_LEFT, onPress); + } + else if (onPress) + { + // Other actions should only happen on release; + return; + } + else if (dnd.mIsOnDragAndDrop) + { + // Intersected with the world while drag and drop is active + // Drop item into the world + MWBase::Environment::get().getWorld()->breakInvisibility( + MWMechanics::getPlayer()); + DropItemAtPointModel drop; + dnd.drop(&drop, nullptr); + } + else if (!ptr.isEmpty()) + { + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + player.activate(ptr); + } + } + } + + void VRInputManager::injectMousePress(int sdlButton, bool onPress) + { + SDL_MouseButtonEvent arg; + if (onPress) + mMouseManager->mousePressed(arg, sdlButton); else - dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count); - dropped.getCellRef().setOwner(""); - - return dropped; + mMouseManager->mouseReleased(arg, sdlButton); } - virtual void removeItem(const MWGui::ItemStack& item, size_t count) { throw std::runtime_error("removeItem not implemented"); } - virtual ModelIndex getIndex(MWGui::ItemStack item) { throw std::runtime_error("getIndex not implemented"); } - virtual void update() {} - virtual size_t getItemCount() { return 0; } - virtual MWGui::ItemStack getItem(ModelIndex index) { throw std::runtime_error("getItem not implemented"); } - -private: - // Where to drop the item - MWRender::RayResult mIntersection; -}; - -void OpenXRInputManager::pointActivation(bool onPress) -{ - auto* world = MWBase::Environment::get().getWorld(); - auto* anim = MWVR::Environment::get().getPlayerAnimation(); - if (world && anim && anim->getPointerTarget().mHit) + void VRInputManager::injectChannelValue( + MWInput::Actions action, + float value) { - auto* node = anim->getPointerTarget().mHitNode; - MWWorld::Ptr ptr = anim->getPointerTarget().mHitObject; - auto& dnd = MWBase::Environment::get().getWindowManager()->getDragAndDrop(); - - if (node && node->getName() == "VRGUILayer") - { - injectMousePress(SDL_BUTTON_LEFT, onPress); - } - else if (onPress) - { - // Other actions should only happen on release; - return; - } - else if (dnd.mIsOnDragAndDrop) - { - // Intersected with the world while drag and drop is active - // Drop item into the world - MWBase::Environment::get().getWorld()->breakInvisibility( - MWMechanics::getPlayer()); - DropItemAtPointModel drop; - dnd.drop(&drop, nullptr); - } - else if (!ptr.isEmpty()) - { - MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); - player.activate(ptr); - } + auto channel = mBindingsManager->ics().getChannel(MWInput::A_MoveLeftRight);// ->setValue(value); + channel->setEnabled(true); } -} -void OpenXRInputManager::injectMousePress(int sdlButton, bool onPress) -{ - SDL_MouseButtonEvent arg; - if (onPress) - mMouseManager->mousePressed(arg, sdlButton); - else - mMouseManager->mouseReleased(arg, sdlButton); -} + // TODO: Configurable haptics: on/off + max intensity + void VRInputManager::applyHapticsLeftHand(float intensity) + { + mXRInput->applyHaptics(TrackedLimb::LEFT_HAND, intensity); + } -void OpenXRInputManager::injectChannelValue( - MWInput::Actions action, - float value) -{ - auto channel = mBindingsManager->ics().getChannel(MWInput::A_MoveLeftRight);// ->setValue(value); - channel->setEnabled(true); -} + void VRInputManager::applyHapticsRightHand(float intensity) + { + mXRInput->applyHaptics(TrackedLimb::RIGHT_HAND, intensity); + } -// TODO: Configurable haptics: on/off + max intensity -void OpenXRInputManager::applyHapticsLeftHand(float intensity) -{ - mXRInput->applyHaptics(TrackedLimb::LEFT_HAND, intensity); -} + void VRInputManager::requestRecenter() + { + mShouldRecenter = true; + } -void OpenXRInputManager::applyHapticsRightHand(float intensity) -{ - mXRInput->applyHaptics(TrackedLimb::RIGHT_HAND, intensity); -} - -void OpenXRInputManager::requestRecenter() -{ - mShouldRecenter = true; -} - -OpenXRInputManager::OpenXRInputManager( - SDL_Window* window, - osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - const std::string& userFile, - bool userFileExists, - const std::string& userControllerBindingsFile, - const std::string& controllerBindingsFile, - bool grab) - : MWInput::InputManager( - window, - viewer, - screenCaptureHandler, - screenCaptureOperation, - userFile, - userFileExists, - userControllerBindingsFile, - controllerBindingsFile, - grab) - , mXRInput(nullptr) -{ - std::vector suggestedBindings; + VRInputManager::VRInputManager( + SDL_Window* window, + osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler, + osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, + const std::string& userFile, + bool userFileExists, + const std::string& userControllerBindingsFile, + const std::string& controllerBindingsFile, + bool grab) + : MWInput::InputManager( + window, + viewer, + screenCaptureHandler, + screenCaptureOperation, + userFile, + userFileExists, + userControllerBindingsFile, + controllerBindingsFile, + grab) + , mXRInput(nullptr) + { + std::vector suggestedBindings; // Set up default bindings for the oculus /* @@ -231,468 +231,468 @@ OpenXRInputManager::OpenXRInputManager( Long: Recenter on player and reset GUI */ - SuggestedBindings oculusTouchBindings{ - "/interaction_profiles/oculus/touch_controller", + SuggestedBindings oculusTouchBindings{ + "/interaction_profiles/oculus/touch_controller", + { + {A_MenuUpDown, ActionPath::ThumbstickY, Side::RIGHT_SIDE}, + {A_MenuLeftRight, ActionPath::ThumbstickX, Side::RIGHT_SIDE}, + {A_MenuSelect, ActionPath::A, Side::RIGHT_SIDE}, + {A_MenuBack, ActionPath::B, Side::RIGHT_SIDE}, + {MWInput::A_LookLeftRight, ActionPath::ThumbstickX, Side::RIGHT_SIDE}, + {MWInput::A_MoveLeftRight, ActionPath::ThumbstickX, Side::LEFT_SIDE}, + {MWInput::A_MoveForwardBackward, ActionPath::ThumbstickY, Side::LEFT_SIDE}, + {MWInput::A_Activate, ActionPath::Squeeze, Side::RIGHT_SIDE}, + {MWInput::A_Use, ActionPath::Trigger, Side::RIGHT_SIDE}, + {MWInput::A_Jump, ActionPath::Trigger, Side::LEFT_SIDE}, + {MWInput::A_ToggleWeapon, ActionPath::A, Side::RIGHT_SIDE}, + {MWInput::A_ToggleSpell, ActionPath::X, Side::LEFT_SIDE}, + //{*mCycleSpellLeft, mThumbstickClickPath, Side::LEFT_SIDE}, + //{*mCycleSpellRight, mThumbstickClickPath, Side::RIGHT_SIDE}, + //{*mCycleWeaponLeft, mThumbstickClickPath, Side::LEFT_SIDE}, + //{*mCycleWeaponRight, mThumbstickClickPath, Side::RIGHT_SIDE}, + {MWInput::A_AlwaysRun, ActionPath::ThumbstickClick, Side::LEFT_SIDE}, + {MWInput::A_AutoMove, ActionPath::ThumbstickClick, Side::RIGHT_SIDE}, + {MWInput::A_ToggleHUD, ActionPath::ThumbstickClick, Side::LEFT_SIDE}, + {MWInput::A_ToggleDebug, ActionPath::ThumbstickClick, Side::RIGHT_SIDE}, + {MWInput::A_Sneak, ActionPath::Squeeze, Side::LEFT_SIDE}, + {MWInput::A_Inventory, ActionPath::B, Side::RIGHT_SIDE}, + {MWInput::A_Rest, ActionPath::Y, Side::LEFT_SIDE}, + {MWInput::A_Journal, ActionPath::B, Side::RIGHT_SIDE}, + {MWInput::A_QuickSave, ActionPath::Y, Side::LEFT_SIDE}, + {MWInput::A_GameMenu, ActionPath::Menu, Side::LEFT_SIDE}, + {A_Recenter, ActionPath::Menu, Side::LEFT_SIDE}, + {A_ActivateTouch, ActionPath::Squeeze, Side::RIGHT_SIDE}, + } + }; + + suggestedBindings.push_back(oculusTouchBindings); + + mXRInput.reset(new OpenXRInput(suggestedBindings)); + } + + VRInputManager::~VRInputManager() + { + } + + void VRInputManager::changeInputMode(bool mode) + { + // VR mode has no concept of these + //mGuiCursorEnabled = false; + MWInput::InputManager::changeInputMode(mode); + MWBase::Environment::get().getWindowManager()->showCrosshair(false); + MWBase::Environment::get().getWindowManager()->setCursorVisible(false); + } + + void VRInputManager::update( + float dt, + bool disableControls, + bool disableEvents) + { + auto begin = std::chrono::steady_clock::now(); + mXRInput->updateControls(); + + + auto* vrGuiManager = Environment::get().getGUIManager(); + if (vrGuiManager) { - {A_MenuUpDown, ActionPath::ThumbstickY, Side::RIGHT_SIDE}, - {A_MenuLeftRight, ActionPath::ThumbstickX, Side::RIGHT_SIDE}, - {A_MenuSelect, ActionPath::A, Side::RIGHT_SIDE}, - {A_MenuBack, ActionPath::B, Side::RIGHT_SIDE}, - {MWInput::A_LookLeftRight, ActionPath::ThumbstickX, Side::RIGHT_SIDE}, - {MWInput::A_MoveLeftRight, ActionPath::ThumbstickX, Side::LEFT_SIDE}, - {MWInput::A_MoveForwardBackward, ActionPath::ThumbstickY, Side::LEFT_SIDE}, - {MWInput::A_Activate, ActionPath::Squeeze, Side::RIGHT_SIDE}, - {MWInput::A_Use, ActionPath::Trigger, Side::RIGHT_SIDE}, - {MWInput::A_Jump, ActionPath::Trigger, Side::LEFT_SIDE}, - {MWInput::A_ToggleWeapon, ActionPath::A, Side::RIGHT_SIDE}, - {MWInput::A_ToggleSpell, ActionPath::X, Side::LEFT_SIDE}, - //{*mCycleSpellLeft, mThumbstickClickPath, Side::LEFT_SIDE}, - //{*mCycleSpellRight, mThumbstickClickPath, Side::RIGHT_SIDE}, - //{*mCycleWeaponLeft, mThumbstickClickPath, Side::LEFT_SIDE}, - //{*mCycleWeaponRight, mThumbstickClickPath, Side::RIGHT_SIDE}, - {MWInput::A_AlwaysRun, ActionPath::ThumbstickClick, Side::LEFT_SIDE}, - {MWInput::A_AutoMove, ActionPath::ThumbstickClick, Side::RIGHT_SIDE}, - {MWInput::A_ToggleHUD, ActionPath::ThumbstickClick, Side::LEFT_SIDE}, - {MWInput::A_ToggleDebug, ActionPath::ThumbstickClick, Side::RIGHT_SIDE}, - {MWInput::A_Sneak, ActionPath::Squeeze, Side::LEFT_SIDE}, - {MWInput::A_Inventory, ActionPath::B, Side::RIGHT_SIDE}, - {MWInput::A_Rest, ActionPath::Y, Side::LEFT_SIDE}, - {MWInput::A_Journal, ActionPath::B, Side::RIGHT_SIDE}, - {MWInput::A_QuickSave, ActionPath::Y, Side::LEFT_SIDE}, - {MWInput::A_GameMenu, ActionPath::Menu, Side::LEFT_SIDE}, - {A_Recenter, ActionPath::Menu, Side::LEFT_SIDE}, - {A_ActivateTouch, ActionPath::Squeeze, Side::RIGHT_SIDE}, - } - }; - - suggestedBindings.push_back(oculusTouchBindings); - - mXRInput.reset(new OpenXRInput(suggestedBindings)); -} - -OpenXRInputManager::~OpenXRInputManager() -{ -} - -void OpenXRInputManager::changeInputMode(bool mode) -{ - // VR mode has no concept of these - //mGuiCursorEnabled = false; - MWInput::InputManager::changeInputMode(mode); - MWBase::Environment::get().getWindowManager()->showCrosshair(false); - MWBase::Environment::get().getWindowManager()->setCursorVisible(false); -} - -void OpenXRInputManager::update( - float dt, - bool disableControls, - bool disableEvents) -{ - auto begin = std::chrono::steady_clock::now(); - mXRInput->updateControls(); - - - auto* vrGuiManager = Environment::get().getGUIManager(); - if (vrGuiManager) - { - bool vrHasFocus = vrGuiManager->updateFocus(); - auto guiCursor = vrGuiManager->guiCursor(); - if (vrHasFocus) - { - mMouseManager->setMousePosition(guiCursor.x(), guiCursor.y()); - } - } - - while (auto* action = mXRInput->nextAction()) - { - processAction(action, dt, disableControls); - } - - updateActivationIndication(); - - MWInput::InputManager::update(dt, disableControls, disableEvents); - - // This is the first update that needs openxr tracking, so i begin the next frame here. - auto* session = Environment::get().getSession(); - if (session) - session->beginPhase(VRSession::FramePhase::Update); - - // The rest of this code assumes the game is running - if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) - { - return; - } - - bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - - // OpenMW assumes all input will come via SDL which i often violate. - // This keeps player controls correctly enabled for my purposes. - mBindingsManager->setPlayerControlsEnabled(!guiMode); - - updateHead(); - - if (!guiMode) - { - auto* world = MWBase::Environment::get().getWorld(); - - auto& player = world->getPlayer(); - auto playerPtr = world->getPlayerPtr(); - if (!mRealisticCombat || mRealisticCombat->ptr != playerPtr) - mRealisticCombat.reset(new RealisticCombat::StateMachine(playerPtr)); - bool enabled = !guiMode && player.getDrawState() == MWMechanics::DrawState_Weapon && !player.isDisabled(); - mRealisticCombat->update(dt, enabled); - } - else if (mRealisticCombat) - mRealisticCombat->update(dt, false); - - - // Update tracking every frame if player is not currently in GUI mode. - // This ensures certain widgets like Notifications will be visible. - if (!guiMode) - { - vrGuiManager->updateTracking(); - } - auto end = std::chrono::steady_clock::now(); - - auto elapsed = std::chrono::duration_cast(end - begin); -} - -void OpenXRInputManager::processAction(const Action* action, float dt, bool disableControls) -{ - static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); - auto* xrGUIManager = Environment::get().getGUIManager(); - - - // OpenMW does not currently provide any way to directly request skipping a video. - // This is copied from the controller manager and is used to skip videos, - // and works because mygui only consumes the escape press if a video is currently playing. - auto kc = MWInput::sdlKeyToMyGUI(SDLK_ESCAPE); - if (action->onActivate()) - { - mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0)); - } - else if (action->onDeactivate()) - { - mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); - } - - if (disableControls) - { - return; - } - - bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); - - if (guiMode) - { - MyGUI::KeyCode key = MyGUI::KeyCode::None; - - // Axis actions - switch (action->openMWActionCode()) - { - case A_MenuLeftRight: - if (action->value() > 0.6f && action->previousValue() < 0.6f) + bool vrHasFocus = vrGuiManager->updateFocus(); + auto guiCursor = vrGuiManager->guiCursor(); + if (vrHasFocus) { - key = MyGUI::KeyCode::ArrowRight; + mMouseManager->setMousePosition(guiCursor.x(), guiCursor.y()); } - if (action->value() < -0.6f && action->previousValue() > -0.6f) - { - key = MyGUI::KeyCode::ArrowLeft; - } - break; - case A_MenuUpDown: - if (action->value() > 0.6f && action->previousValue() < 0.6f) - { - key = MyGUI::KeyCode::ArrowUp; - } - if (action->value() < -0.6f && action->previousValue() > -0.6f) - { - key = MyGUI::KeyCode::ArrowDown; - } - break; - default: break; } - // OnActivate actions + while (auto* action = mXRInput->nextAction()) + { + processAction(action, dt, disableControls); + } + + updateActivationIndication(); + + MWInput::InputManager::update(dt, disableControls, disableEvents); + + // This is the first update that needs openxr tracking, so i begin the next frame here. + auto* session = Environment::get().getSession(); + if (session) + session->beginPhase(VRSession::FramePhase::Update); + + // The rest of this code assumes the game is running + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + { + return; + } + + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + + // OpenMW assumes all input will come via SDL which i often violate. + // This keeps player controls correctly enabled for my purposes. + mBindingsManager->setPlayerControlsEnabled(!guiMode); + + updateHead(); + + if (!guiMode) + { + auto* world = MWBase::Environment::get().getWorld(); + + auto& player = world->getPlayer(); + auto playerPtr = world->getPlayerPtr(); + if (!mRealisticCombat || mRealisticCombat->ptr != playerPtr) + mRealisticCombat.reset(new RealisticCombat::StateMachine(playerPtr)); + bool enabled = !guiMode && player.getDrawState() == MWMechanics::DrawState_Weapon && !player.isDisabled(); + mRealisticCombat->update(dt, enabled); + } + else if (mRealisticCombat) + mRealisticCombat->update(dt, false); + + + // Update tracking every frame if player is not currently in GUI mode. + // This ensures certain widgets like Notifications will be visible. + if (!guiMode) + { + vrGuiManager->updateTracking(); + } + auto end = std::chrono::steady_clock::now(); + + auto elapsed = std::chrono::duration_cast(end - begin); + } + + void VRInputManager::processAction(const Action* action, float dt, bool disableControls) + { + static const bool isToggleSneak = Settings::Manager::getBool("toggle sneak", "Input"); + auto* xrGUIManager = Environment::get().getGUIManager(); + + + // OpenMW does not currently provide any way to directly request skipping a video. + // This is copied from the controller manager and is used to skip videos, + // and works because mygui only consumes the escape press if a video is currently playing. + auto kc = MWInput::sdlKeyToMyGUI(SDLK_ESCAPE); if (action->onActivate()) { + mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyPress(kc, 0)); + } + else if (action->onDeactivate()) + { + mBindingsManager->setPlayerControlsEnabled(!MyGUI::InputManager::getInstance().injectKeyRelease(kc)); + } + + if (disableControls) + { + return; + } + + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + + if (guiMode) + { + MyGUI::KeyCode key = MyGUI::KeyCode::None; + + // Axis actions switch (action->openMWActionCode()) { - case MWInput::A_GameMenu: - mActionManager->toggleMainMenu(); + case A_MenuLeftRight: + if (action->value() > 0.6f && action->previousValue() < 0.6f) + { + key = MyGUI::KeyCode::ArrowRight; + } + if (action->value() < -0.6f && action->previousValue() > -0.6f) + { + key = MyGUI::KeyCode::ArrowLeft; + } break; - case MWInput::A_Screenshot: - mActionManager->screenshot(); - break; - case A_Recenter: - xrGUIManager->updateTracking(); - requestRecenter(); - break; - case A_MenuSelect: - if (!MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Space, 0, 0)) - executeAction(MWInput::A_Activate); - break; - case A_MenuBack: - if (MyGUI::InputManager::getInstance().isModalAny()) - MWBase::Environment::get().getWindowManager()->exitCurrentModal(); - else - MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); - break; - case MWInput::A_Use: - pointActivation(true); - default: + case A_MenuUpDown: + if (action->value() > 0.6f && action->previousValue() < 0.6f) + { + key = MyGUI::KeyCode::ArrowUp; + } + if (action->value() < -0.6f && action->previousValue() > -0.6f) + { + key = MyGUI::KeyCode::ArrowDown; + } break; + default: break; } - } - // A few actions need to fire on deactivation - if (action->onDeactivate()) - { - switch (action->openMWActionCode()) + // OnActivate actions + if (action->onActivate()) { - case MWInput::A_Use: - mBindingsManager->ics().getChannel(MWInput::A_Use)->setValue(0.f); - pointActivation(false); - break; - default: - break; - } - } - - if (key != MyGUI::KeyCode::None) - { - MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, 0); - } - } - - else - { - - // Hold actions - switch (action->openMWActionCode()) - { - case A_ActivateTouch: - resetIdleTime(); - mActivationIndication = action->isActive(); - break; - case MWInput::A_LookLeftRight: - mYaw += osg::DegreesToRadians(action->value()) * 200.f * dt; - break; - case MWInput::A_MoveLeftRight: - mBindingsManager->ics().getChannel(MWInput::A_MoveLeftRight)->setValue(action->value() / 2.f + 0.5f); - break; - case MWInput::A_MoveForwardBackward: - mBindingsManager->ics().getChannel(MWInput::A_MoveForwardBackward)->setValue(-action->value() / 2.f + 0.5f); - break; - case MWInput::A_Sneak: - { - if (!isToggleSneak) - mBindingsManager->ics().getChannel(MWInput::A_Sneak)->setValue(action->isActive() ? 1.f : 0.f); - break; - } - case MWInput::A_Use: - if (!(mActivationIndication || MWBase::Environment::get().getWindowManager()->isGuiMode())) - mBindingsManager->ics().getChannel(MWInput::A_Use)->setValue(action->value()); - break; - default: - break; - } - - // OnActivate actions - if (action->onActivate()) - { - switch (action->openMWActionCode()) - { - case MWInput::A_GameMenu: - mActionManager->toggleMainMenu(); - break; - case MWInput::A_Screenshot: - mActionManager->screenshot(); - break; - case MWInput::A_Inventory: - //mActionManager->toggleInventory(); - injectMousePress(SDL_BUTTON_RIGHT, true); - break; - case MWInput::A_Console: - mActionManager->toggleConsole(); - break; - case MWInput::A_Journal: - mActionManager->toggleJournal(); - break; - case MWInput::A_AutoMove: - mActionManager->toggleAutoMove(); - break; - case MWInput::A_AlwaysRun: - mActionManager->toggleWalking(); - break; - case MWInput::A_ToggleWeapon: - mActionManager->toggleWeapon(); - break; - case MWInput::A_Rest: - mActionManager->rest(); - break; - case MWInput::A_ToggleSpell: - mActionManager->toggleSpell(); - break; - case MWInput::A_QuickKey1: - mActionManager->quickKey(1); - break; - case MWInput::A_QuickKey2: - mActionManager->quickKey(2); - break; - case MWInput::A_QuickKey3: - mActionManager->quickKey(3); - break; - case MWInput::A_QuickKey4: - mActionManager->quickKey(4); - break; - case MWInput::A_QuickKey5: - mActionManager->quickKey(5); - break; - case MWInput::A_QuickKey6: - mActionManager->quickKey(6); - break; - case MWInput::A_QuickKey7: - mActionManager->quickKey(7); - break; - case MWInput::A_QuickKey8: - mActionManager->quickKey(8); - break; - case MWInput::A_QuickKey9: - mActionManager->quickKey(9); - break; - case MWInput::A_QuickKey10: - mActionManager->quickKey(10); - break; - case MWInput::A_QuickKeysMenu: - mActionManager->showQuickKeysMenu(); - break; - case MWInput::A_ToggleHUD: - Log(Debug::Verbose) << "Toggle HUD"; - MWBase::Environment::get().getWindowManager()->toggleHud(); - break; - case MWInput::A_ToggleDebug: - Log(Debug::Verbose) << "Toggle Debug"; - MWBase::Environment::get().getWindowManager()->toggleDebugWindow(); - break; - case MWInput::A_QuickSave: - mActionManager->quickSave(); - break; - case MWInput::A_QuickLoad: - mActionManager->quickLoad(); - break; - case MWInput::A_CycleSpellLeft: - if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Magic)) - MWBase::Environment::get().getWindowManager()->cycleSpell(false); - break; - case MWInput::A_CycleSpellRight: - if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Magic)) - MWBase::Environment::get().getWindowManager()->cycleSpell(true); - break; - case MWInput::A_CycleWeaponLeft: - if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - MWBase::Environment::get().getWindowManager()->cycleWeapon(false); - break; - case MWInput::A_CycleWeaponRight: - if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) - MWBase::Environment::get().getWindowManager()->cycleWeapon(true); - break; - case MWInput::A_Jump: - mActionManager->setAttemptJump(true); - break; - case A_Recenter: - xrGUIManager->updateTracking(); - if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) + switch (action->openMWActionCode()) + { + case MWInput::A_GameMenu: + mActionManager->toggleMainMenu(); + break; + case MWInput::A_Screenshot: + mActionManager->screenshot(); + break; + case A_Recenter: + xrGUIManager->updateTracking(); requestRecenter(); - break; - case MWInput::A_Use: - if (mActivationIndication || MWBase::Environment::get().getWindowManager()->isGuiMode()) + break; + case A_MenuSelect: + if (!MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Space, 0, 0)) + executeAction(MWInput::A_Activate); + break; + case A_MenuBack: + if (MyGUI::InputManager::getInstance().isModalAny()) + MWBase::Environment::get().getWindowManager()->exitCurrentModal(); + else + MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); + break; + case MWInput::A_Use: pointActivation(true); - default: - break; + default: + break; + } + } + + // A few actions need to fire on deactivation + if (action->onDeactivate()) + { + switch (action->openMWActionCode()) + { + case MWInput::A_Use: + mBindingsManager->ics().getChannel(MWInput::A_Use)->setValue(0.f); + pointActivation(false); + break; + default: + break; + } + } + + if (key != MyGUI::KeyCode::None) + { + MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, 0); } } - // A few actions need to fire on deactivation - if (action->onDeactivate()) + else { + + // Hold actions switch (action->openMWActionCode()) { - case MWInput::A_Use: - mBindingsManager->ics().getChannel(MWInput::A_Use)->setValue(0.f); - if (mActivationIndication || MWBase::Environment::get().getWindowManager()->isGuiMode()) - pointActivation(false); + case A_ActivateTouch: + resetIdleTime(); + mActivationIndication = action->isActive(); + break; + case MWInput::A_LookLeftRight: + mYaw += osg::DegreesToRadians(action->value()) * 200.f * dt; + break; + case MWInput::A_MoveLeftRight: + mBindingsManager->ics().getChannel(MWInput::A_MoveLeftRight)->setValue(action->value() / 2.f + 0.5f); + break; + case MWInput::A_MoveForwardBackward: + mBindingsManager->ics().getChannel(MWInput::A_MoveForwardBackward)->setValue(-action->value() / 2.f + 0.5f); break; case MWInput::A_Sneak: - if (isToggleSneak) - mActionManager->toggleSneaking(); + { + if (!isToggleSneak) + mBindingsManager->ics().getChannel(MWInput::A_Sneak)->setValue(action->isActive() ? 1.f : 0.f); + break; + } + case MWInput::A_Use: + if (!(mActivationIndication || MWBase::Environment::get().getWindowManager()->isGuiMode())) + mBindingsManager->ics().getChannel(MWInput::A_Use)->setValue(action->value()); break; - case MWInput::A_Inventory: - injectMousePress(SDL_BUTTON_RIGHT, false); default: break; } + + // OnActivate actions + if (action->onActivate()) + { + switch (action->openMWActionCode()) + { + case MWInput::A_GameMenu: + mActionManager->toggleMainMenu(); + break; + case MWInput::A_Screenshot: + mActionManager->screenshot(); + break; + case MWInput::A_Inventory: + //mActionManager->toggleInventory(); + injectMousePress(SDL_BUTTON_RIGHT, true); + break; + case MWInput::A_Console: + mActionManager->toggleConsole(); + break; + case MWInput::A_Journal: + mActionManager->toggleJournal(); + break; + case MWInput::A_AutoMove: + mActionManager->toggleAutoMove(); + break; + case MWInput::A_AlwaysRun: + mActionManager->toggleWalking(); + break; + case MWInput::A_ToggleWeapon: + mActionManager->toggleWeapon(); + break; + case MWInput::A_Rest: + mActionManager->rest(); + break; + case MWInput::A_ToggleSpell: + mActionManager->toggleSpell(); + break; + case MWInput::A_QuickKey1: + mActionManager->quickKey(1); + break; + case MWInput::A_QuickKey2: + mActionManager->quickKey(2); + break; + case MWInput::A_QuickKey3: + mActionManager->quickKey(3); + break; + case MWInput::A_QuickKey4: + mActionManager->quickKey(4); + break; + case MWInput::A_QuickKey5: + mActionManager->quickKey(5); + break; + case MWInput::A_QuickKey6: + mActionManager->quickKey(6); + break; + case MWInput::A_QuickKey7: + mActionManager->quickKey(7); + break; + case MWInput::A_QuickKey8: + mActionManager->quickKey(8); + break; + case MWInput::A_QuickKey9: + mActionManager->quickKey(9); + break; + case MWInput::A_QuickKey10: + mActionManager->quickKey(10); + break; + case MWInput::A_QuickKeysMenu: + mActionManager->showQuickKeysMenu(); + break; + case MWInput::A_ToggleHUD: + Log(Debug::Verbose) << "Toggle HUD"; + MWBase::Environment::get().getWindowManager()->toggleHud(); + break; + case MWInput::A_ToggleDebug: + Log(Debug::Verbose) << "Toggle Debug"; + MWBase::Environment::get().getWindowManager()->toggleDebugWindow(); + break; + case MWInput::A_QuickSave: + mActionManager->quickSave(); + break; + case MWInput::A_QuickLoad: + mActionManager->quickLoad(); + break; + case MWInput::A_CycleSpellLeft: + if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Magic)) + MWBase::Environment::get().getWindowManager()->cycleSpell(false); + break; + case MWInput::A_CycleSpellRight: + if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Magic)) + MWBase::Environment::get().getWindowManager()->cycleSpell(true); + break; + case MWInput::A_CycleWeaponLeft: + if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) + MWBase::Environment::get().getWindowManager()->cycleWeapon(false); + break; + case MWInput::A_CycleWeaponRight: + if (mActionManager->checkAllowedToUseItems() && MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) + MWBase::Environment::get().getWindowManager()->cycleWeapon(true); + break; + case MWInput::A_Jump: + mActionManager->setAttemptJump(true); + break; + case A_Recenter: + xrGUIManager->updateTracking(); + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) + requestRecenter(); + break; + case MWInput::A_Use: + if (mActivationIndication || MWBase::Environment::get().getWindowManager()->isGuiMode()) + pointActivation(true); + default: + break; + } + } + + // A few actions need to fire on deactivation + if (action->onDeactivate()) + { + switch (action->openMWActionCode()) + { + case MWInput::A_Use: + mBindingsManager->ics().getChannel(MWInput::A_Use)->setValue(0.f); + if (mActivationIndication || MWBase::Environment::get().getWindowManager()->isGuiMode()) + pointActivation(false); + break; + case MWInput::A_Sneak: + if (isToggleSneak) + mActionManager->toggleSneaking(); + break; + case MWInput::A_Inventory: + injectMousePress(SDL_BUTTON_RIGHT, false); + default: + break; + } + } } } -} -osg::Quat OpenXRInputManager::stageRotation() -{ - return osg::Quat(mYaw, osg::Vec3(0, 0, -1)); -} - -void OpenXRInputManager::updateHead() -{ - auto* session = Environment::get().getSession(); - auto currentHeadPose = session->predictedPoses(VRSession::FramePhase::Update).head; - currentHeadPose.position *= Environment::get().unitsPerMeter(); - osg::Vec3 vrMovement = currentHeadPose.position - mHeadPose.position; - mHeadPose = currentHeadPose; - mHeadOffset += stageRotation() * vrMovement; - - if (mShouldRecenter) + osg::Quat VRInputManager::stageRotation() { - // Move position of head to center of character - // Z should not be affected - mHeadOffset = osg::Vec3(0, 0, 0); - mHeadOffset.z() = mHeadPose.position.z(); - - // Adjust orientation to zero yaw - float yaw = 0.f; - float pitch = 0.f; - float roll = 0.f; - getEulerAngles(mHeadPose.orientation, yaw, pitch, roll); - mYaw = -yaw; - - mShouldRecenter = false; - Log(Debug::Verbose) << "Recentered (" << mYaw << ")"; + return osg::Quat(mYaw, osg::Vec3(0, 0, -1)); } - else + + void VRInputManager::updateHead() { - MWBase::World* world = MWBase::Environment::get().getWorld(); - auto& player = world->getPlayer(); - auto playerPtr = player.getPlayer(); + auto* session = Environment::get().getSession(); + auto currentHeadPose = session->predictedPoses(VRSession::FramePhase::Update).head; + currentHeadPose.position *= Environment::get().unitsPerMeter(); + osg::Vec3 vrMovement = currentHeadPose.position - mHeadPose.position; + mHeadPose = currentHeadPose; + mHeadOffset += stageRotation() * vrMovement; - float yaw = 0.f; - float pitch = 0.f; - float roll = 0.f; - getEulerAngles(mHeadPose.orientation, yaw, pitch, roll); - - yaw += mYaw; - - mVrAngles[0] = pitch; - mVrAngles[1] = roll; - mVrAngles[2] = yaw; - - if (!player.isDisabled()) + if (mShouldRecenter) { - world->rotateObject(playerPtr, mVrAngles[0], mVrAngles[1], mVrAngles[2], MWBase::RotationFlag_none); - } - else { - // Update the camera directly to avoid rotating the disabled player - world->getRenderingManager().getCamera()->rotateCamera(-pitch, -roll, -yaw, false); - } - } + // Move position of head to center of character + // Z should not be affected + mHeadOffset = osg::Vec3(0, 0, 0); + mHeadOffset.z() = mHeadPose.position.z(); -} + // Adjust orientation to zero yaw + float yaw = 0.f; + float pitch = 0.f; + float roll = 0.f; + getEulerAngles(mHeadPose.orientation, yaw, pitch, roll); + mYaw = -yaw; + + mShouldRecenter = false; + Log(Debug::Verbose) << "Recentered (" << mYaw << ")"; + } + else + { + MWBase::World* world = MWBase::Environment::get().getWorld(); + auto& player = world->getPlayer(); + auto playerPtr = player.getPlayer(); + + float yaw = 0.f; + float pitch = 0.f; + float roll = 0.f; + getEulerAngles(mHeadPose.orientation, yaw, pitch, roll); + + yaw += mYaw; + + mVrAngles[0] = pitch; + mVrAngles[1] = roll; + mVrAngles[2] = yaw; + + if (!player.isDisabled()) + { + world->rotateObject(playerPtr, mVrAngles[0], mVrAngles[1], mVrAngles[2], MWBase::RotationFlag_none); + } + else { + // Update the camera directly to avoid rotating the disabled player + world->getRenderingManager().getCamera()->rotateCamera(-pitch, -roll, -yaw, false); + } + } + + } } diff --git a/apps/openmw/mwvr/vrinputmanager.hpp b/apps/openmw/mwvr/vrinputmanager.hpp index a55a38532..fc1d9baad 100644 --- a/apps/openmw/mwvr/vrinputmanager.hpp +++ b/apps/openmw/mwvr/vrinputmanager.hpp @@ -13,69 +13,69 @@ namespace MWVR { -struct OpenXRInput; + struct OpenXRInput; -/// As far as I can tell, SDL does not support VR controllers. -/// So I subclass the input manager and insert VR controls. -class OpenXRInputManager : public MWInput::InputManager -{ -public: - OpenXRInputManager( - SDL_Window* window, - osg::ref_ptr viewer, - osg::ref_ptr screenCaptureHandler, - osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, - const std::string& userFile, bool userFileExists, - const std::string& userControllerBindingsFile, - const std::string& controllerBindingsFile, bool grab); + /// As far as I can tell, SDL does not support VR controllers. + /// So I subclass the input manager and insert VR controls. + class VRInputManager : public MWInput::InputManager + { + public: + VRInputManager( + SDL_Window* window, + osg::ref_ptr viewer, + osg::ref_ptr screenCaptureHandler, + osgViewer::ScreenCaptureHandler::CaptureOperation* screenCaptureOperation, + const std::string& userFile, bool userFileExists, + const std::string& userControllerBindingsFile, + const std::string& controllerBindingsFile, bool grab); - virtual ~OpenXRInputManager(); + virtual ~VRInputManager(); - /// Overriden to force vr modes such as hiding cursors and crosshairs - virtual void changeInputMode(bool guiMode); + /// Overriden to force vr modes such as hiding cursors and crosshairs + virtual void changeInputMode(bool guiMode); - /// Overriden to update XR inputs - virtual void update(float dt, bool disableControls = false, bool disableEvents = false); + /// Overriden to update XR inputs + virtual void update(float dt, bool disableControls = false, bool disableEvents = false); - /// Current head offset from character position - osg::Vec3 headOffset() const { return mHeadOffset; }; + /// Current head offset from character position + osg::Vec3 headOffset() const { return mHeadOffset; }; - /// Update head offset. Should only be called by the movement solver when reducing head offset. - void setHeadOffset(osg::Vec3 offset) { mHeadOffset = offset; }; + /// Update head offset. Should only be called by the movement solver when reducing head offset. + void setHeadOffset(osg::Vec3 offset) { mHeadOffset = offset; }; - /// Quaternion that aligns VR stage coordinates with world coordinates. - osg::Quat stageRotation(); + /// Quaternion that aligns VR stage coordinates with world coordinates. + osg::Quat stageRotation(); - /// Set current offset to 0 and re-align VR stage. - void requestRecenter(); + /// Set current offset to 0 and re-align VR stage. + void requestRecenter(); - /// Tracking pose of the given limb at the given predicted time - Pose getLimbPose(int64_t time, TrackedLimb limb); + /// Tracking pose of the given limb at the given predicted time + Pose getLimbPose(int64_t time, TrackedLimb limb); -protected: - void updateHead(); + protected: + void updateHead(); - void processAction(const class Action* action, float dt, bool disableControls); + void processAction(const class Action* action, float dt, bool disableControls); - void updateActivationIndication(void); - void pointActivation(bool onPress); + void updateActivationIndication(void); + void pointActivation(bool onPress); - void injectMousePress(int sdlButton, bool onPress); - void injectChannelValue(MWInput::Actions action, float value); + void injectMousePress(int sdlButton, bool onPress); + void injectChannelValue(MWInput::Actions action, float value); - void applyHapticsLeftHand(float intensity) override; - void applyHapticsRightHand(float intensity) override; + void applyHapticsLeftHand(float intensity) override; + void applyHapticsRightHand(float intensity) override; - std::unique_ptr mXRInput; - std::unique_ptr mRealisticCombat; - Pose mHeadPose{}; - osg::Vec3 mHeadOffset{ 0,0,0 }; - bool mShouldRecenter{ true }; - bool mActivationIndication{ false }; - float mYaw{ 0.f }; + std::unique_ptr mXRInput; + std::unique_ptr mRealisticCombat; + Pose mHeadPose{}; + osg::Vec3 mHeadOffset{ 0,0,0 }; + bool mShouldRecenter{ true }; + bool mActivationIndication{ false }; + float mYaw{ 0.f }; - float mVrAngles[3]{ 0.f,0.f,0.f }; -}; + float mVrAngles[3]{ 0.f,0.f,0.f }; + }; } #endif diff --git a/apps/openmw/mwvr/vrsession.cpp b/apps/openmw/mwvr/vrsession.cpp index fdc0b128f..2b1035485 100644 --- a/apps/openmw/mwvr/vrsession.cpp +++ b/apps/openmw/mwvr/vrsession.cpp @@ -36,323 +36,323 @@ namespace MWVR { -VRSession::VRSession() -{ -} - -VRSession::~VRSession() -{ -} - -osg::Matrix VRSession::projectionMatrix(FramePhase phase, Side side) -{ - assert(((int)side) < 2); - auto fov = predictedPoses(VRSession::FramePhase::Update).view[(int)side].fov; - float near_ = Settings::Manager::getFloat("near clip", "Camera"); - float far_ = Settings::Manager::getFloat("viewing distance", "Camera"); - return fov.perspectiveMatrix(near_, far_); -} - -osg::Matrix VRSession::viewMatrix(FramePhase phase, Side side) -{ - MWVR::Pose pose{}; - pose = predictedPoses(phase).view[(int)side].pose; - - - if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + VRSession::VRSession() { - pose = predictedPoses(phase).eye[(int)side]; + } + + VRSession::~VRSession() + { + } + + osg::Matrix VRSession::projectionMatrix(FramePhase phase, Side side) + { + assert(((int)side) < 2); + auto fov = predictedPoses(VRSession::FramePhase::Update).view[(int)side].fov; + float near_ = Settings::Manager::getFloat("near clip", "Camera"); + float far_ = Settings::Manager::getFloat("viewing distance", "Camera"); + return fov.perspectiveMatrix(near_, far_); + } + + osg::Matrix VRSession::viewMatrix(FramePhase phase, Side side) + { + MWVR::Pose pose{}; + pose = predictedPoses(phase).view[(int)side].pose; + + + if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) + { + pose = predictedPoses(phase).eye[(int)side]; + osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); + osg::Quat orientation = pose.orientation; + osg::Vec3d forward = orientation * osg::Vec3d(0, 1, 0); + osg::Vec3d up = orientation * osg::Vec3d(0, 0, 1); + osg::Matrix viewMatrix; + viewMatrix.makeLookAt(position, position + forward, up); + + return viewMatrix; + } + osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); osg::Quat orientation = pose.orientation; - osg::Vec3d forward = orientation * osg::Vec3d(0, 1, 0); - osg::Vec3d up = orientation * osg::Vec3d(0, 0, 1); + + float y = position.y(); + float z = position.z(); + position.y() = z; + position.z() = -y; + + y = orientation.y(); + z = orientation.z(); + orientation.y() = z; + orientation.z() = -y; + osg::Matrix viewMatrix; - viewMatrix.makeLookAt(position, position + forward, up); + viewMatrix.setTrans(-position); + viewMatrix.postMultRotate(orientation.conj()); return viewMatrix; } - osg::Vec3 position = pose.position * Environment::get().unitsPerMeter(); - osg::Quat orientation = pose.orientation; - - float y = position.y(); - float z = position.z(); - position.y() = z; - position.z() = -y; - - y = orientation.y(); - z = orientation.z(); - orientation.y() = z; - orientation.z() = -y; - - osg::Matrix viewMatrix; - viewMatrix.setTrans(-position); - viewMatrix.postMultRotate(orientation.conj()); - - return viewMatrix; -} - -bool VRSession::isRunning() const { - auto* xr = Environment::get().getManager(); - return xr->xrSessionRunning(); -} - -void VRSession::swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer) -{ - auto* xr = Environment::get().getManager(); - - beginPhase(FramePhase::Swap); - - auto* frameMeta = getFrame(FramePhase::Swap).get(); - - if (frameMeta->mShouldRender && isRunning()) - { - auto leftView = viewer.mViews["LeftEye"]; - auto rightView = viewer.mViews["RightEye"]; - - viewer.blitEyesToMirrorTexture(gc); - gc->swapBuffersImplementation(); - leftView->swapBuffers(gc); - rightView->swapBuffers(gc); - - std::array layerStack{}; - layerStack[(int)Side::LEFT_SIDE].swapchain = &leftView->swapchain(); - layerStack[(int)Side::RIGHT_SIDE].swapchain = &rightView->swapchain(); - layerStack[(int)Side::LEFT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::LEFT_SIDE] / mPlayerScale; - layerStack[(int)Side::RIGHT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::RIGHT_SIDE] / mPlayerScale; - layerStack[(int)Side::LEFT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::LEFT_SIDE].fov; - layerStack[(int)Side::RIGHT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::RIGHT_SIDE].fov; - - Log(Debug::Debug) << frameMeta->mFrameNo << ": EndFrame " <endFrame(frameMeta->mPredictedDisplayTime, 1, layerStack); + bool VRSession::isRunning() const { + auto* xr = Environment::get().getManager(); + return xr->xrSessionRunning(); } + void VRSession::swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer) { - std::unique_lock lock(mMutex); + auto* xr = Environment::get().getManager(); - // Some of these values are useless until the prediction time bug is resolved by oculus. - auto now = std::chrono::steady_clock::now(); - mLastFrameInterval = std::chrono::duration_cast(now - mLastRenderedFrameTimestamp); - mLastRenderedFrameTimestamp = now; - mLastRenderedFrame = getFrame(FramePhase::Swap)->mFrameNo; + beginPhase(FramePhase::Swap); - // Using this to track framerate over the course of gameplay, rather than just seeing the instantaneous - auto seconds = std::chrono::duration_cast>(now - mStart).count(); - static int sBaseFrames = 0; - if (seconds > 10.f) + auto* frameMeta = getFrame(FramePhase::Swap).get(); + + if (frameMeta->mShouldRender && isRunning()) { - Log(Debug::Verbose) << "Fps: " << (static_cast(mLastRenderedFrame - sBaseFrames) / seconds); - mStart = now; - sBaseFrames = mLastRenderedFrame; + auto leftView = viewer.getView("LeftEye"); + auto rightView = viewer.getView("RightEye"); + + viewer.blitEyesToMirrorTexture(gc); + gc->swapBuffersImplementation(); + leftView->swapBuffers(gc); + rightView->swapBuffers(gc); + + std::array layerStack{}; + layerStack[(int)Side::LEFT_SIDE].swapchain = &leftView->swapchain(); + layerStack[(int)Side::RIGHT_SIDE].swapchain = &rightView->swapchain(); + layerStack[(int)Side::LEFT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::LEFT_SIDE] / mPlayerScale; + layerStack[(int)Side::RIGHT_SIDE].pose = frameMeta->mPredictedPoses.eye[(int)Side::RIGHT_SIDE] / mPlayerScale; + layerStack[(int)Side::LEFT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::LEFT_SIDE].fov; + layerStack[(int)Side::RIGHT_SIDE].fov = frameMeta->mPredictedPoses.view[(int)Side::RIGHT_SIDE].fov; + + Log(Debug::Debug) << frameMeta->mFrameNo << ": EndFrame " << std::this_thread::get_id(); + xr->endFrame(frameMeta->mPredictedDisplayTime, 1, layerStack); } - getFrame(FramePhase::Swap) = nullptr; - mFramesInFlight--; - } - mCondition.notify_one(); -} - -void VRSession::beginPhase(FramePhase phase) -{ - Log(Debug::Debug) << "beginPhase(" << ((int)phase) << ") " << std::this_thread::get_id(); - - if (getFrame(phase)) - { - Log(Debug::Warning) << "advanceFramePhase called with a frame alreay in the target phase"; - return; - } - - - if (phase == FramePhase::Update) - { - prepareFrame(); - } - else - { - std::unique_lock lock(mMutex); - FramePhase previousPhase = static_cast((int)phase - 1); - if (!getFrame(previousPhase)) - throw std::logic_error("beginPhase called without a frame"); - getFrame(phase) = std::move(getFrame(previousPhase)); - } - - - // TODO: Invokation should depend on earliest render rather than necessarily phase. - // Specifically. Without shadows this is fine because nothing is being rendered - // during cull or earlier. - // Thought: Add an Shadowmapping phase and invoke it from the shadow code - // But with shadows rendering occurs during cull and we must do frame sync before those calls. - // If you want to pay the FPS toll and play with shadows, change FramePhase::Draw to FramePhase::Cull or enjoy your eyes getting torn apart by jitters. - if (phase == FramePhase::Cull && getFrame(phase)->mShouldRender) - doFrameSync(); -} - -void VRSession::doFrameSync() -{ - { - std::unique_lock lock(mMutex); - while (mLastRenderedFrame != mFrames - 1) { - mCondition.wait(lock); + std::unique_lock lock(mMutex); + + // Some of these values are useless until the prediction time bug is resolved by oculus. + auto now = std::chrono::steady_clock::now(); + mLastFrameInterval = std::chrono::duration_cast(now - mLastRenderedFrameTimestamp); + mLastRenderedFrameTimestamp = now; + mLastRenderedFrame = getFrame(FramePhase::Swap)->mFrameNo; + + // Using this to track framerate over the course of gameplay, rather than just seeing the instantaneous + auto seconds = std::chrono::duration_cast>(now - mStart).count(); + static int sBaseFrames = 0; + if (seconds > 10.f) + { + Log(Debug::Verbose) << "Fps: " << (static_cast(mLastRenderedFrame - sBaseFrames) / seconds); + mStart = now; + sBaseFrames = mLastRenderedFrame; + } + + getFrame(FramePhase::Swap) = nullptr; + mFramesInFlight--; } + mCondition.notify_one(); } - auto* xr = Environment::get().getManager(); - Log(Debug::Debug) << mFrames << ": WaitFrame " << std::this_thread::get_id(); - xr->waitFrame(); - Log(Debug::Debug) << mFrames << ": BeginFrame " << std::this_thread::get_id(); - xr->beginFrame(); -} - -std::unique_ptr& VRSession::getFrame(FramePhase phase) -{ - if ((unsigned int)phase >= mFrame.size()) - throw std::logic_error("Invalid frame phase"); - return mFrame[(int)phase]; -} - -void VRSession::prepareFrame() -{ - - std::unique_lock lock(mMutex); - mFrames++; - assert(!mPredrawFrame); - - auto* xr = Environment::get().getManager(); - xr->handleEvents(); - - //auto frameState = xr->impl().frameState(); -// auto predictedDisplayTime = frameState.predictedDisplayTime; -// if (predictedDisplayTime == 0) -// { -// // First time, need to invent a frame time since openxr won't help us without a call to waitframe. -// predictedDisplayTime = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); -// } -// else -// { -// // Predict display time based on real framerate -// float intervalsf = static_cast(mLastFrameInterval.count()) / static_cast(mLastPredictedDisplayPeriod); -// int intervals = std::max((int)std::roundf(intervalsf), 1); -// predictedDisplayTime = mLastPredictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * mLastPredictedDisplayPeriod; -// } - -//////////////////////// OCULUS BUG - //////////////////// Oculus will suddenly start monotonically increasing their predicted display time by precisely 1 second - //////////////////// regardless of real time passed, causing predictions to go crazy due to the time difference. - //////////////////// Therefore, for the time being, i ignore oculus' predicted display time altogether. - long long predictedDisplayTime = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); - if (mFrames > 1) + void VRSession::beginPhase(FramePhase phase) { - auto framePeriod = xr->getLastPredictedDisplayPeriod(); - float intervalsf = static_cast(mLastFrameInterval.count()) / static_cast(framePeriod); - int intervals = std::max((int)std::roundf(intervalsf), 1); - predictedDisplayTime = predictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * framePeriod; + Log(Debug::Debug) << "beginPhase(" << ((int)phase) << ") " << std::this_thread::get_id(); + + if (getFrame(phase)) + { + Log(Debug::Warning) << "advanceFramePhase called with a frame alreay in the target phase"; + return; + } + + + if (phase == FramePhase::Update) + { + prepareFrame(); + } + else + { + std::unique_lock lock(mMutex); + FramePhase previousPhase = static_cast((int)phase - 1); + if (!getFrame(previousPhase)) + throw std::logic_error("beginPhase called without a frame"); + getFrame(phase) = std::move(getFrame(previousPhase)); + } + + + // TODO: Invokation should depend on earliest render rather than necessarily phase. + // Specifically. Without shadows this is fine because nothing is being rendered + // during cull or earlier. + // Thought: Add an Shadowmapping phase and invoke it from the shadow code + // But with shadows rendering occurs during cull and we must do frame sync before those calls. + // If you want to pay the FPS toll and play with shadows, change FramePhase::Draw to FramePhase::Cull or enjoy your eyes getting torn apart by jitters. + if (phase == FramePhase::Cull && getFrame(phase)->mShouldRender) + doFrameSync(); } - - - PoseSet predictedPoses{}; - - xr->enablePredictions(); - predictedPoses.head = xr->getPredictedHeadPose(predictedDisplayTime, ReferenceSpace::STAGE) * mPlayerScale; - auto hmdViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::VIEW); - predictedPoses.view[(int)Side::LEFT_SIDE].pose = hmdViews[(int)Side::LEFT_SIDE].pose * mPlayerScale; - predictedPoses.view[(int)Side::RIGHT_SIDE].pose = hmdViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale; - predictedPoses.view[(int)Side::LEFT_SIDE].fov = hmdViews[(int)Side::LEFT_SIDE].fov; - predictedPoses.view[(int)Side::RIGHT_SIDE].fov = hmdViews[(int)Side::RIGHT_SIDE].fov; - auto stageViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::STAGE); - predictedPoses.eye[(int)Side::LEFT_SIDE] = stageViews[(int)Side::LEFT_SIDE].pose * mPlayerScale; - predictedPoses.eye[(int)Side::RIGHT_SIDE] = stageViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale; - - auto* input = Environment::get().getInputManager(); - if (input) + void VRSession::doFrameSync() { - predictedPoses.hands[(int)Side::LEFT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::LEFT_HAND) * mPlayerScale; - predictedPoses.hands[(int)Side::RIGHT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::RIGHT_HAND) * mPlayerScale; + { + std::unique_lock lock(mMutex); + while (mLastRenderedFrame != mFrames - 1) + { + mCondition.wait(lock); + } + } + + auto* xr = Environment::get().getManager(); + Log(Debug::Debug) << mFrames << ": WaitFrame " << std::this_thread::get_id(); + xr->waitFrame(); + Log(Debug::Debug) << mFrames << ": BeginFrame " << std::this_thread::get_id(); + xr->beginFrame(); } - xr->disablePredictions(); - auto& frame = getFrame(FramePhase::Update); - frame.reset(new VRFrameMeta); - frame->mPredictedDisplayTime = predictedDisplayTime; - frame->mFrameNo = mFrames; - frame->mPredictedPoses = predictedPoses; - frame->mShouldRender = isRunning(); - mFramesInFlight++; -} - -const PoseSet& VRSession::predictedPoses(FramePhase phase) -{ - auto& frame = getFrame(phase); - - if (phase == FramePhase::Update && !frame) - beginPhase(FramePhase::Update); - - if (!frame) - throw std::logic_error("Attempted to get poses from a phase with no current pose"); - return frame->mPredictedPoses; -} - -// OSG doesn't provide API to extract euler angles from a quat, but i need it. -// Credits goes to Dennis Bunfield, i just copied his formula https://narkive.com/v0re6547.4 -void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll) -{ - // Now do the computation - osg::Matrixd m2(osg::Matrixd::rotate(quat)); - double* mat = (double*)m2.ptr(); - double angle_x = 0.0; - double angle_y = 0.0; - double angle_z = 0.0; - double D, C, tr_x, tr_y; - angle_y = D = asin(mat[2]); /* Calculate Y-axis angle */ - C = cos(angle_y); - if (fabs(C) > 0.005) /* Test for Gimball lock? */ + std::unique_ptr& VRSession::getFrame(FramePhase phase) { - tr_x = mat[10] / C; /* No, so get X-axis angle */ - tr_y = -mat[6] / C; - angle_x = atan2(tr_y, tr_x); - tr_x = mat[0] / C; /* Get Z-axis angle */ - tr_y = -mat[1] / C; - angle_z = atan2(tr_y, tr_x); + if ((unsigned int)phase >= mFrame.size()) + throw std::logic_error("Invalid frame phase"); + return mFrame[(int)phase]; } - else /* Gimball lock has occurred */ + + void VRSession::prepareFrame() { - angle_x = 0; /* Set X-axis angle to zero - */ - tr_x = mat[5]; /* And calculate Z-axis angle - */ - tr_y = mat[4]; - angle_z = atan2(tr_y, tr_x); + + std::unique_lock lock(mMutex); + mFrames++; + assert(!mPredrawFrame); + + auto* xr = Environment::get().getManager(); + xr->handleEvents(); + + //auto frameState = xr->impl().frameState(); + // auto predictedDisplayTime = frameState.predictedDisplayTime; + // if (predictedDisplayTime == 0) + // { + // // First time, need to invent a frame time since openxr won't help us without a call to waitframe. + // predictedDisplayTime = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + // } + // else + // { + // // Predict display time based on real framerate + // float intervalsf = static_cast(mLastFrameInterval.count()) / static_cast(mLastPredictedDisplayPeriod); + // int intervals = std::max((int)std::roundf(intervalsf), 1); + // predictedDisplayTime = mLastPredictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * mLastPredictedDisplayPeriod; + // } + + //////////////////////// OCULUS BUG + //////////////////// Oculus will suddenly start monotonically increasing their predicted display time by precisely 1 second + //////////////////// regardless of real time passed, causing predictions to go crazy due to the time difference. + //////////////////// Therefore, for the time being, i ignore oculus' predicted display time altogether. + long long predictedDisplayTime = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + if (mFrames > 1) + { + auto framePeriod = xr->getLastPredictedDisplayPeriod(); + float intervalsf = static_cast(mLastFrameInterval.count()) / static_cast(framePeriod); + int intervals = std::max((int)std::roundf(intervalsf), 1); + predictedDisplayTime = predictedDisplayTime + intervals * (mFrames - mLastRenderedFrame) * framePeriod; + } + + + + PoseSet predictedPoses{}; + + xr->enablePredictions(); + predictedPoses.head = xr->getPredictedHeadPose(predictedDisplayTime, ReferenceSpace::STAGE) * mPlayerScale; + auto hmdViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::VIEW); + predictedPoses.view[(int)Side::LEFT_SIDE].pose = hmdViews[(int)Side::LEFT_SIDE].pose * mPlayerScale; + predictedPoses.view[(int)Side::RIGHT_SIDE].pose = hmdViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale; + predictedPoses.view[(int)Side::LEFT_SIDE].fov = hmdViews[(int)Side::LEFT_SIDE].fov; + predictedPoses.view[(int)Side::RIGHT_SIDE].fov = hmdViews[(int)Side::RIGHT_SIDE].fov; + auto stageViews = xr->getPredictedViews(predictedDisplayTime, ReferenceSpace::STAGE); + predictedPoses.eye[(int)Side::LEFT_SIDE] = stageViews[(int)Side::LEFT_SIDE].pose * mPlayerScale; + predictedPoses.eye[(int)Side::RIGHT_SIDE] = stageViews[(int)Side::RIGHT_SIDE].pose * mPlayerScale; + + auto* input = Environment::get().getInputManager(); + if (input) + { + predictedPoses.hands[(int)Side::LEFT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::LEFT_HAND) * mPlayerScale; + predictedPoses.hands[(int)Side::RIGHT_SIDE] = input->getLimbPose(predictedDisplayTime, TrackedLimb::RIGHT_HAND) * mPlayerScale; + } + xr->disablePredictions(); + + auto& frame = getFrame(FramePhase::Update); + frame.reset(new VRFrameMeta); + frame->mPredictedDisplayTime = predictedDisplayTime; + frame->mFrameNo = mFrames; + frame->mPredictedPoses = predictedPoses; + frame->mShouldRender = isRunning(); + mFramesInFlight++; } - yaw = angle_z; - pitch = angle_x; - roll = angle_y; -} + const PoseSet& VRSession::predictedPoses(FramePhase phase) + { + auto& frame = getFrame(phase); -void VRSession::movementAngles(float& yaw, float& pitch) -{ - assert(mPredrawFrame); - if (!getFrame(FramePhase::Update)) - beginPhase(FramePhase::Update); - auto frameMeta = getFrame(FramePhase::Update).get(); - - float headYaw = 0.f; - float headPitch = 0.f; - float headsWillRoll = 0.f; + if (phase == FramePhase::Update && !frame) + beginPhase(FramePhase::Update); - float handYaw = 0.f; - float handPitch = 0.f; - float handRoll = 0.f; + if (!frame) + throw std::logic_error("Attempted to get poses from a phase with no current pose"); + return frame->mPredictedPoses; + } - getEulerAngles(frameMeta->mPredictedPoses.head.orientation, headYaw, headPitch, headsWillRoll); - getEulerAngles(frameMeta->mPredictedPoses.hands[(int)Side::LEFT_SIDE].orientation, handYaw, handPitch, handRoll); + // OSG doesn't provide API to extract euler angles from a quat, but i need it. + // Credits goes to Dennis Bunfield, i just copied his formula https://narkive.com/v0re6547.4 + void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll) + { + // Now do the computation + osg::Matrixd m2(osg::Matrixd::rotate(quat)); + double* mat = (double*)m2.ptr(); + double angle_x = 0.0; + double angle_y = 0.0; + double angle_z = 0.0; + double D, C, tr_x, tr_y; + angle_y = D = asin(mat[2]); /* Calculate Y-axis angle */ + C = cos(angle_y); + if (fabs(C) > 0.005) /* Test for Gimball lock? */ + { + tr_x = mat[10] / C; /* No, so get X-axis angle */ + tr_y = -mat[6] / C; + angle_x = atan2(tr_y, tr_x); + tr_x = mat[0] / C; /* Get Z-axis angle */ + tr_y = -mat[1] / C; + angle_z = atan2(tr_y, tr_x); + } + else /* Gimball lock has occurred */ + { + angle_x = 0; /* Set X-axis angle to zero + */ + tr_x = mat[5]; /* And calculate Z-axis angle + */ + tr_y = mat[4]; + angle_z = atan2(tr_y, tr_x); + } - //Log(Debug::Verbose) << "lhandViewYaw=" << yaw << ", headYaw=" << headYaw << ", handYaw=" << handYaw << ", diff=" << (handYaw - headYaw); - //Log(Debug::Verbose) << "lhandViewPitch=" << pitch << ", headPitch=" << headPitch << ", handPitch=" << handPitch << ", diff=" << (handPitch - headPitch); - yaw = handYaw - headYaw; - pitch = handPitch - headPitch; -} + yaw = angle_z; + pitch = angle_x; + roll = angle_y; + } + + void VRSession::movementAngles(float& yaw, float& pitch) + { + assert(mPredrawFrame); + if (!getFrame(FramePhase::Update)) + beginPhase(FramePhase::Update); + auto frameMeta = getFrame(FramePhase::Update).get(); + + float headYaw = 0.f; + float headPitch = 0.f; + float headsWillRoll = 0.f; + + float handYaw = 0.f; + float handPitch = 0.f; + float handRoll = 0.f; + + getEulerAngles(frameMeta->mPredictedPoses.head.orientation, headYaw, headPitch, headsWillRoll); + getEulerAngles(frameMeta->mPredictedPoses.hands[(int)Side::LEFT_SIDE].orientation, handYaw, handPitch, handRoll); + + //Log(Debug::Verbose) << "lhandViewYaw=" << yaw << ", headYaw=" << headYaw << ", handYaw=" << handYaw << ", diff=" << (handYaw - headYaw); + //Log(Debug::Verbose) << "lhandViewPitch=" << pitch << ", headPitch=" << headPitch << ", handPitch=" << handPitch << ", diff=" << (handPitch - headPitch); + yaw = handYaw - headYaw; + pitch = handPitch - headPitch; + } } diff --git a/apps/openmw/mwvr/vrsession.hpp b/apps/openmw/mwvr/vrsession.hpp index e57b45249..8ad3d66dc 100644 --- a/apps/openmw/mwvr/vrsession.hpp +++ b/apps/openmw/mwvr/vrsession.hpp @@ -15,84 +15,85 @@ namespace MWVR { -extern void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll); + extern void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll); -//! Manages VR logic, such as managing frames, predicting their poses, and handling frame synchronization. -//! Should not be confused with the openxr session object. -class VRSession -{ -public: - using seconds = std::chrono::duration; - using nanoseconds = std::chrono::nanoseconds; - using clock = std::chrono::steady_clock; - using time_point = clock::time_point; - - //! Describes different phases of updating/rendering each frame. - //! While two phases suffice for disambiguating current and next frame, - //! greater granularity allows better control of synchronization - //! with the VR runtime. - enum class FramePhase + /// \brief Manages VR logic, such as managing frames, predicting their poses, and handling frame synchronization with the VR runtime. + /// Should not be confused with the openxr session object. + class VRSession { - Update = 0, //!< The frame currently in update traversals - Cull, //!< The frame currently in cull - Draw, //!< The frame currently in draw - Swap, //!< The frame being swapped - NumPhases + public: + using seconds = std::chrono::duration; + using nanoseconds = std::chrono::nanoseconds; + using clock = std::chrono::steady_clock; + using time_point = clock::time_point; + + //! Describes different phases of updating/rendering each frame. + //! While two phases suffice for disambiguating current and next frame, + //! greater granularity allows better control of synchronization + //! with the VR runtime. + enum class FramePhase + { + Update = 0, //!< The frame currently in update traversals + Cull, //!< The frame currently in cull + Draw, //!< The frame currently in draw + Swap, //!< The frame being swapped + NumPhases + }; + + struct VRFrameMeta + { + long long mFrameNo{ 0 }; + long long mPredictedDisplayTime{ 0 }; + PoseSet mPredictedPoses{}; + bool mShouldRender{ false }; + }; + + public: + VRSession(); + ~VRSession(); + + void swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer); + + const PoseSet& predictedPoses(FramePhase phase); + + //! Starts a new frame + void prepareFrame(); + + //! Synchronize with openxr + void doFrameSync(); + + //! Angles to be used for overriding movement direction + void movementAngles(float& yaw, float& pitch); + + void beginPhase(FramePhase phase); + std::unique_ptr& getFrame(FramePhase phase); + + bool isRunning() const; + + float playerScale() const { return mPlayerScale; } + float setPlayerScale(float scale) { return mPlayerScale = scale; } + + osg::Matrix viewMatrix(FramePhase phase, Side side); + osg::Matrix projectionMatrix(FramePhase phase, Side side); + + int mFramesInFlight{ 0 }; + std::array, (int)FramePhase::NumPhases> mFrame{ nullptr }; + + private: + std::mutex mMutex{}; + std::condition_variable mCondition{}; + + long long mFrames{ 0 }; + long long mLastRenderedFrame{ 0 }; + long long mLastPredictedDisplayTime{ 0 }; + long long mLastPredictedDisplayPeriod{ 0 }; + std::chrono::steady_clock::time_point mStart{ std::chrono::steady_clock::now() }; + std::chrono::nanoseconds mLastFrameInterval{}; + std::chrono::steady_clock::time_point mLastRenderedFrameTimestamp{ std::chrono::steady_clock::now() }; + + float mPlayerScale{ 1.f }; }; - struct VRFrameMeta - { - long long mFrameNo{ 0 }; - long long mPredictedDisplayTime{ 0 }; - PoseSet mPredictedPoses{}; - bool mShouldRender{ false }; - }; - -public: - VRSession(); - ~VRSession(); - - void swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer); - - const PoseSet& predictedPoses(FramePhase phase); - - //! Starts a new frame - void prepareFrame(); - - //! Synchronize with openxr - void doFrameSync(); - - //! Angles to be used for overriding movement direction - void movementAngles(float& yaw, float& pitch); - - void beginPhase(FramePhase phase); - std::unique_ptr& getFrame(FramePhase phase); - - bool isRunning() const; - - float playerScale() const { return mPlayerScale; } - float setPlayerScale(float scale) { return mPlayerScale = scale; } - - osg::Matrix viewMatrix(FramePhase phase, Side side); - osg::Matrix projectionMatrix(FramePhase phase, Side side); - - int mFramesInFlight{ 0 }; - std::array, (int)FramePhase::NumPhases> mFrame{ nullptr }; - - std::mutex mMutex{}; - std::condition_variable mCondition{}; - - long long mFrames{ 0 }; - long long mLastRenderedFrame{ 0 }; - long long mLastPredictedDisplayTime{ 0 }; - long long mLastPredictedDisplayPeriod{ 0 }; - std::chrono::steady_clock::time_point mStart{ std::chrono::steady_clock::now() }; - std::chrono::nanoseconds mLastFrameInterval{}; - std::chrono::steady_clock::time_point mLastRenderedFrameTimestamp{std::chrono::steady_clock::now()}; - - float mPlayerScale{ 1.f }; -}; - } #endif diff --git a/apps/openmw/mwvr/vrtexture.cpp b/apps/openmw/mwvr/vrtexture.cpp index 88dc99709..24d834b6a 100644 --- a/apps/openmw/mwvr/vrtexture.cpp +++ b/apps/openmw/mwvr/vrtexture.cpp @@ -1,11 +1,8 @@ -#include "vrviewer.hpp" #include "vrtexture.hpp" + #include -#include + #include -#include -#include -#include #ifndef GL_TEXTURE_MAX_LEVEL #define GL_TEXTURE_MAX_LEVEL 0x813D @@ -18,9 +15,9 @@ namespace MWVR : mState(state) , mWidth(width) , mHeight(height) - , mSamples(msaaSamples) - , mColorBuffer(colorBuffer) , mDepthBuffer(depthBuffer) + , mColorBuffer(colorBuffer) + , mSamples(msaaSamples) { auto* gl = osg::GLExtensions::Get(state->getContextID(), false); @@ -94,10 +91,10 @@ namespace MWVR if (mFBO) gl->glDeleteFramebuffers(1, &mFBO); } - else if(mFBO) + else if (mFBO) // Without access to opengl methods, i'll just let the FBOs leak. Log(Debug::Warning) << "destroy() called without a State. Leaking FBO"; - + if (mDepthBuffer) glDeleteTextures(1, &mDepthBuffer); if (mColorBuffer) diff --git a/apps/openmw/mwvr/vrtypes.cpp b/apps/openmw/mwvr/vrtypes.cpp index 8fd9afe5e..d89c2246a 100644 --- a/apps/openmw/mwvr/vrtypes.cpp +++ b/apps/openmw/mwvr/vrtypes.cpp @@ -1,202 +1,203 @@ #include "vrtypes.hpp" + #include namespace MWVR { -Pose Pose::operator+(const Pose& rhs) -{ - Pose pose = *this; - pose.position += this->orientation * rhs.position; - pose.orientation = rhs.orientation * this->orientation; - return pose; -} - -const Pose& Pose::operator+=(const Pose& rhs) -{ - *this = *this + rhs; - return *this; -} - -Pose Pose::operator*(float scalar) -{ - Pose pose = *this; - pose.position *= scalar; - return pose; -} - -const Pose& Pose::operator*=(float scalar) -{ - *this = *this * scalar; - return *this; -} - -Pose Pose::operator/(float scalar) -{ - Pose pose = *this; - pose.position /= scalar; - return pose; -} -const Pose& Pose::operator/=(float scalar) -{ - *this = *this / scalar; - return *this; -} - -bool Pose::operator==(const Pose& rhs) const -{ - return position == rhs.position && orientation == rhs.orientation; -} - -bool FieldOfView::operator==(const FieldOfView& rhs) const -{ - return angleDown == rhs.angleDown - && angleUp == rhs.angleUp - && angleLeft == rhs.angleLeft - && angleRight == rhs.angleRight; -} - -// near and far named with an underscore because of windows' headers galaxy brain defines. -osg::Matrix FieldOfView::perspectiveMatrix(float near_, float far_) -{ - const float tanLeft = tanf(angleLeft); - const float tanRight = tanf(angleRight); - const float tanDown = tanf(angleDown); - const float tanUp = tanf(angleUp); - - const float tanWidth = tanRight - tanLeft; - const float tanHeight = tanUp - tanDown; - - const float offset = near_; - - float matrix[16] = {}; - - matrix[0] = 2 / tanWidth; - matrix[4] = 0; - matrix[8] = (tanRight + tanLeft) / tanWidth; - matrix[12] = 0; - - matrix[1] = 0; - matrix[5] = 2 / tanHeight; - matrix[9] = (tanUp + tanDown) / tanHeight; - matrix[13] = 0; - - if (far_ <= near_) { - matrix[2] = 0; - matrix[6] = 0; - matrix[10] = -1; - matrix[14] = -(near_ + offset); - } - else { - matrix[2] = 0; - matrix[6] = 0; - matrix[10] = -(far_ + offset) / (far_ - near_); - matrix[14] = -(far_ * (near_ + offset)) / (far_ - near_); - } - - matrix[3] = 0; - matrix[7] = 0; - matrix[11] = -1; - matrix[15] = 0; - - return osg::Matrix(matrix); -} -bool PoseSet::operator==(const PoseSet& rhs) const -{ - return eye[0] == rhs.eye[0] - && eye[1] == rhs.eye[1] - && hands[0] == rhs.hands[0] - && hands[1] == rhs.hands[1] - && view[0] == rhs.view[0] - && view[1] == rhs.view[1] - && head == rhs.head; - -} -bool View::operator==(const View& rhs) const -{ - return pose == rhs.pose && fov == rhs.fov; -} - -std::ostream& operator <<( - std::ostream& os, - const MWVR::Pose& pose) -{ - os << "position=" << pose.position << ", orientation=" << pose.orientation; - return os; -} - -std::ostream& operator <<( - std::ostream& os, - const MWVR::FieldOfView& fov) -{ - os << "left=" << fov.angleLeft << ", right=" << fov.angleRight << ", down=" << fov.angleDown << ", up=" << fov.angleUp; - return os; -} - -std::ostream& operator <<( - std::ostream& os, - const MWVR::View& view) -{ - os << "pose=< " << view.pose << " >, fov=< " << view.fov << " >"; - return os; -} - -std::ostream& operator <<( - std::ostream& os, - const MWVR::PoseSet& poseSet) -{ - os << "eye[" << Side::LEFT_SIDE<< "]: " << poseSet.eye[(int)Side::LEFT_SIDE] << std::endl; - os << "eye[" << Side::RIGHT_SIDE<< "]: " << poseSet.eye[(int)Side::RIGHT_SIDE] << std::endl; - os << "hands[" << Side::LEFT_SIDE<< "]: " << poseSet.hands[(int)Side::LEFT_SIDE] << std::endl; - os << "hands[" << Side::RIGHT_SIDE<< "]: " << poseSet.hands[(int)Side::RIGHT_SIDE] << std::endl; - os << "head: " << poseSet.head << std::endl; - os << "view[" << Side::LEFT_SIDE<< "]: " << poseSet.view[(int)Side::LEFT_SIDE] << std::endl; - os << "view[" << Side::RIGHT_SIDE<< "]: " << poseSet.view[(int)Side::RIGHT_SIDE] << std::endl; - return os; -} - -std::ostream& operator <<( - std::ostream& os, - TrackedLimb limb) -{ - switch (limb) + Pose Pose::operator+(const Pose& rhs) { - case TrackedLimb::HEAD: - os << "HEAD"; break; - case TrackedLimb::LEFT_HAND: - os << "LEFT_HAND"; break; - case TrackedLimb::RIGHT_HAND: - os << "RIGHT_HAND"; break; + Pose pose = *this; + pose.position += this->orientation * rhs.position; + pose.orientation = rhs.orientation * this->orientation; + return pose; } - return os; -} -std::ostream& operator <<( - std::ostream& os, - ReferenceSpace limb) -{ - switch (limb) + const Pose& Pose::operator+=(const Pose& rhs) { - case ReferenceSpace::STAGE: - os << "STAGE"; break; - case ReferenceSpace::VIEW: - os << "VIEW"; break; + *this = *this + rhs; + return *this; } - return os; -} -std::ostream& operator <<( - std::ostream& os, - Side side) -{ - switch (side) + Pose Pose::operator*(float scalar) { - case Side::LEFT_SIDE: - os << "LEFT_SIDE"; break; - case Side::RIGHT_SIDE: - os << "RIGHT_SIDE"; break; + Pose pose = *this; + pose.position *= scalar; + return pose; + } + + const Pose& Pose::operator*=(float scalar) + { + *this = *this * scalar; + return *this; + } + + Pose Pose::operator/(float scalar) + { + Pose pose = *this; + pose.position /= scalar; + return pose; + } + const Pose& Pose::operator/=(float scalar) + { + *this = *this / scalar; + return *this; + } + + bool Pose::operator==(const Pose& rhs) const + { + return position == rhs.position && orientation == rhs.orientation; + } + + bool FieldOfView::operator==(const FieldOfView& rhs) const + { + return angleDown == rhs.angleDown + && angleUp == rhs.angleUp + && angleLeft == rhs.angleLeft + && angleRight == rhs.angleRight; + } + + // near and far named with an underscore because of windows' headers galaxy brain defines. + osg::Matrix FieldOfView::perspectiveMatrix(float near_, float far_) + { + const float tanLeft = tanf(angleLeft); + const float tanRight = tanf(angleRight); + const float tanDown = tanf(angleDown); + const float tanUp = tanf(angleUp); + + const float tanWidth = tanRight - tanLeft; + const float tanHeight = tanUp - tanDown; + + const float offset = near_; + + float matrix[16] = {}; + + matrix[0] = 2 / tanWidth; + matrix[4] = 0; + matrix[8] = (tanRight + tanLeft) / tanWidth; + matrix[12] = 0; + + matrix[1] = 0; + matrix[5] = 2 / tanHeight; + matrix[9] = (tanUp + tanDown) / tanHeight; + matrix[13] = 0; + + if (far_ <= near_) { + matrix[2] = 0; + matrix[6] = 0; + matrix[10] = -1; + matrix[14] = -(near_ + offset); + } + else { + matrix[2] = 0; + matrix[6] = 0; + matrix[10] = -(far_ + offset) / (far_ - near_); + matrix[14] = -(far_ * (near_ + offset)) / (far_ - near_); + } + + matrix[3] = 0; + matrix[7] = 0; + matrix[11] = -1; + matrix[15] = 0; + + return osg::Matrix(matrix); + } + bool PoseSet::operator==(const PoseSet& rhs) const + { + return eye[0] == rhs.eye[0] + && eye[1] == rhs.eye[1] + && hands[0] == rhs.hands[0] + && hands[1] == rhs.hands[1] + && view[0] == rhs.view[0] + && view[1] == rhs.view[1] + && head == rhs.head; + + } + bool View::operator==(const View& rhs) const + { + return pose == rhs.pose && fov == rhs.fov; + } + + std::ostream& operator <<( + std::ostream& os, + const MWVR::Pose& pose) + { + os << "position=" << pose.position << ", orientation=" << pose.orientation; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const MWVR::FieldOfView& fov) + { + os << "left=" << fov.angleLeft << ", right=" << fov.angleRight << ", down=" << fov.angleDown << ", up=" << fov.angleUp; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const MWVR::View& view) + { + os << "pose=< " << view.pose << " >, fov=< " << view.fov << " >"; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const MWVR::PoseSet& poseSet) + { + os << "eye[" << Side::LEFT_SIDE << "]: " << poseSet.eye[(int)Side::LEFT_SIDE] << std::endl; + os << "eye[" << Side::RIGHT_SIDE << "]: " << poseSet.eye[(int)Side::RIGHT_SIDE] << std::endl; + os << "hands[" << Side::LEFT_SIDE << "]: " << poseSet.hands[(int)Side::LEFT_SIDE] << std::endl; + os << "hands[" << Side::RIGHT_SIDE << "]: " << poseSet.hands[(int)Side::RIGHT_SIDE] << std::endl; + os << "head: " << poseSet.head << std::endl; + os << "view[" << Side::LEFT_SIDE << "]: " << poseSet.view[(int)Side::LEFT_SIDE] << std::endl; + os << "view[" << Side::RIGHT_SIDE << "]: " << poseSet.view[(int)Side::RIGHT_SIDE] << std::endl; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + TrackedLimb limb) + { + switch (limb) + { + case TrackedLimb::HEAD: + os << "HEAD"; break; + case TrackedLimb::LEFT_HAND: + os << "LEFT_HAND"; break; + case TrackedLimb::RIGHT_HAND: + os << "RIGHT_HAND"; break; + } + return os; + } + + std::ostream& operator <<( + std::ostream& os, + ReferenceSpace limb) + { + switch (limb) + { + case ReferenceSpace::STAGE: + os << "STAGE"; break; + case ReferenceSpace::VIEW: + os << "VIEW"; break; + } + return os; + } + + std::ostream& operator <<( + std::ostream& os, + Side side) + { + switch (side) + { + case Side::LEFT_SIDE: + os << "LEFT_SIDE"; break; + case Side::RIGHT_SIDE: + os << "RIGHT_SIDE"; break; + } + return os; } - return os; -} } diff --git a/apps/openmw/mwvr/vrtypes.hpp b/apps/openmw/mwvr/vrtypes.hpp index b4ee7c3a3..f844b3849 100644 --- a/apps/openmw/mwvr/vrtypes.hpp +++ b/apps/openmw/mwvr/vrtypes.hpp @@ -12,113 +12,109 @@ namespace MWVR { -class OpenXRSwapchain; -class OpenXRManager; + class OpenXRSwapchain; + class OpenXRManager; -//! Describes what limb to track. -enum class TrackedLimb -{ - LEFT_HAND, - RIGHT_HAND, - HEAD -}; + //! Describes what limb to track. + enum class TrackedLimb + { + LEFT_HAND, + RIGHT_HAND, + HEAD + }; -//! Describes what space to track the limb in -enum class ReferenceSpace -{ - STAGE = 0, //!< Track limb in the VR stage space. Meaning a space with a floor level origin and fixed horizontal orientation. - VIEW = 1 //!< Track limb in the VR view space. Meaning a space with the head as origin and orientation. -}; + //! Describes what space to track the limb in + enum class ReferenceSpace + { + STAGE = 0, //!< Track limb in the VR stage space. Meaning a space with a floor level origin and fixed horizontal orientation. + VIEW = 1 //!< Track limb in the VR view space. Meaning a space with the head as origin and orientation. + }; -//! Self-descriptive -enum class Side -{ - LEFT_SIDE = 0, - RIGHT_SIDE = 1 -}; + //! Self-descriptive + enum class Side + { + LEFT_SIDE = 0, + RIGHT_SIDE = 1 + }; -//! Represents the relative pose in space of some limb or eye. -struct Pose -{ - //! Position in space - osg::Vec3 position{ 0,0,0 }; - //! Orientation in space. - osg::Quat orientation{ 0,0,0,1 }; + //! Represents the relative pose in space of some limb or eye. + struct Pose + { + //! Position in space + osg::Vec3 position{ 0,0,0 }; + //! Orientation in space. + osg::Quat orientation{ 0,0,0,1 }; - //! Add one pose to another - Pose operator+(const Pose& rhs); - const Pose& operator+=(const Pose& rhs); + //! Add one pose to another + Pose operator+(const Pose& rhs); + const Pose& operator+=(const Pose& rhs); - //! Scale a pose (does not affect orientation) - Pose operator*(float scalar); - const Pose& operator*=(float scalar); - Pose operator/(float scalar); - const Pose& operator/=(float scalar); + //! Scale a pose (does not affect orientation) + Pose operator*(float scalar); + const Pose& operator*=(float scalar); + Pose operator/(float scalar); + const Pose& operator/=(float scalar); - bool operator==(const Pose& rhs) const; -}; + bool operator==(const Pose& rhs) const; + }; -//! Fov of a single eye -struct FieldOfView { - float angleLeft; - float angleRight; - float angleUp; - float angleDown; + //! Fov of a single eye + struct FieldOfView { + float angleLeft; + float angleRight; + float angleUp; + float angleDown; - bool operator==(const FieldOfView& rhs) const; + bool operator==(const FieldOfView& rhs) const; - //! Generate a perspective matrix from this fov - osg::Matrix perspectiveMatrix(float near, float far); -}; + //! Generate a perspective matrix from this fov + osg::Matrix perspectiveMatrix(float near, float far); + }; -//! Represents an eye in VR including both pose and fov. A view's pose is relative to the head. -struct View -{ - Pose pose; - FieldOfView fov; - bool operator==(const View& rhs) const; -}; + //! Represents an eye in VR including both pose and fov. A view's pose is relative to the head. + struct View + { + Pose pose; + FieldOfView fov; + bool operator==(const View& rhs) const; + }; -//! The complete set of poses tracked each frame by MWVR. -struct PoseSet -{ - Pose eye[2]{}; //!< Stage-relative - Pose hands[2]{}; //!< Stage-relative - Pose head{}; //!< Stage-relative - View view[2]{}; //!< Head-relative + //! The complete set of poses tracked each frame by MWVR. + struct PoseSet + { + Pose eye[2]{}; //!< Stage-relative + Pose hands[2]{}; //!< Stage-relative + Pose head{}; //!< Stage-relative + View view[2]{}; //!< Head-relative - bool operator==(const PoseSet& rhs) const; -}; + bool operator==(const PoseSet& rhs) const; + }; -struct CompositionLayerProjectionView -{ - class OpenXRSwapchain* swapchain; - Pose pose; - FieldOfView fov; -}; + struct CompositionLayerProjectionView + { + class OpenXRSwapchain* swapchain; + Pose pose; + FieldOfView fov; + }; -struct SwapchainConfig -{ - uint32_t recommendedWidth = -1; - uint32_t maxWidth = -1; - uint32_t recommendedHeight = -1; - uint32_t maxHeight = -1; - uint32_t recommendedSamples = -1; - uint32_t maxSamples = -1; -}; - -// Serialization methods for VR types. -std::ostream& operator <<(std::ostream& os, const Pose& pose); -std::ostream& operator <<(std::ostream& os, const FieldOfView& fov); -std::ostream& operator <<(std::ostream& os, const View& view); -std::ostream& operator <<(std::ostream& os, const PoseSet& poseSet); -std::ostream& operator <<(std::ostream& os, TrackedLimb limb); -std::ostream& operator <<(std::ostream& os, ReferenceSpace space); -std::ostream& operator <<(std::ostream& os, Side side); + struct SwapchainConfig + { + uint32_t recommendedWidth = -1; + uint32_t maxWidth = -1; + uint32_t recommendedHeight = -1; + uint32_t maxHeight = -1; + uint32_t recommendedSamples = -1; + uint32_t maxSamples = -1; + }; + // Serialization methods for VR types. + std::ostream& operator <<(std::ostream& os, const Pose& pose); + std::ostream& operator <<(std::ostream& os, const FieldOfView& fov); + std::ostream& operator <<(std::ostream& os, const View& view); + std::ostream& operator <<(std::ostream& os, const PoseSet& poseSet); + std::ostream& operator <<(std::ostream& os, TrackedLimb limb); + std::ostream& operator <<(std::ostream& os, ReferenceSpace space); + std::ostream& operator <<(std::ostream& os, Side side); } - -// I put these in the global namespace to guarantee lookup. - #endif diff --git a/apps/openmw/mwvr/vrview.cpp b/apps/openmw/mwvr/vrview.cpp index 3f8ed86f1..6dc54743f 100644 --- a/apps/openmw/mwvr/vrview.cpp +++ b/apps/openmw/mwvr/vrview.cpp @@ -1,24 +1,11 @@ #include "vrview.hpp" -#include "vrsession.hpp" + #include "openxrmanager.hpp" #include "openxrmanagerimpl.hpp" -#include "../mwinput/inputmanagerimp.hpp" -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwrender/renderingmanager.hpp" -#include "../mwrender/water.hpp" +#include "vrsession.hpp" #include "vrenvironment.hpp" - #include -#include - -#include - -#include -#include -#include -#include #include @@ -92,8 +79,8 @@ namespace MWVR { } } void VRView::UpdateSlaveCallback::updateSlave( - osg::View& view, - osg::View::Slave& slave) + osg::View& view, + osg::View::Slave& slave) { auto* camera = slave._camera.get(); auto name = camera->getName(); diff --git a/apps/openmw/mwvr/vrview.hpp b/apps/openmw/mwvr/vrview.hpp index b5b951757..432c236bb 100644 --- a/apps/openmw/mwvr/vrview.hpp +++ b/apps/openmw/mwvr/vrview.hpp @@ -9,7 +9,7 @@ struct XrSwapchainSubImage; namespace MWVR { - + /// \brief Manipulates a slave camera by replacing its framebuffer with one destined for openxr class VRView : public osg::Referenced { public: @@ -48,7 +48,7 @@ namespace MWVR osg::Camera* createCamera(int order, const osg::Vec4& clearColor, osg::GraphicsContext* gc); //! Get the view surface OpenXRSwapchain& swapchain(void) { return *mSwapchain; } - + //! Present to the openxr swapchain void swapBuffers(osg::GraphicsContext* gc); public: diff --git a/apps/openmw/mwvr/vrviewer.cpp b/apps/openmw/mwvr/vrviewer.cpp index 940c22cb1..91c7cbde4 100644 --- a/apps/openmw/mwvr/vrviewer.cpp +++ b/apps/openmw/mwvr/vrviewer.cpp @@ -1,22 +1,19 @@ #include "vrviewer.hpp" -#include "vrsession.hpp" + #include "openxrmanagerimpl.hpp" -#include "openxrinput.hpp" #include "vrenvironment.hpp" -#include "Windows.h" -#include "../mwmechanics/actorutil.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/environment.hpp" -#include "../mwworld/class.hpp" -#include "../mwworld/player.hpp" -#include "../mwworld/esmstore.hpp" +#include "vrsession.hpp" + #include "../mwrender/vismask.hpp" -#include -#include namespace MWVR { + const std::array VRViewer::sViewNames = { + "LeftEye", + "RightEye" + }; + VRViewer::VRViewer( osg::ref_ptr viewer) : mRealizeOperation(new RealizeOperation()) @@ -42,87 +39,62 @@ namespace MWVR void VRViewer::realize(osg::GraphicsContext* context) { std::unique_lock lock(mMutex); - + if (mConfigured) { return; } - if (!context->isCurrent()) - if (!context->makeCurrent()) - { - throw std::logic_error("VRViewer::configure() failed to make graphics context current."); - return; - } - + // Give the main camera an initial draw callback that disables camera setup (we don't want it) auto mainCamera = mCameras["MainCamera"] = mViewer->getCamera(); mainCamera->setName("Main"); mainCamera->setInitialDrawCallback(new VRView::InitialDrawCallback()); - osg::Vec4 clearColor = mainCamera->getClearColor(); - auto* xr = Environment::get().getManager(); if (!xr->realized()) xr->realize(context); + // Run through initial events to start session + // For the rest of runtime this is handled by vrsession xr->handleEvents(); + // Small feature culling + bool smallFeatureCulling = Settings::Manager::getBool("small feature culling", "Camera"); + auto smallFeatureCullingPixelSize = Settings::Manager::getFloat("small feature culling pixel size", "Camera"); + osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING | osg::Camera::FAR_PLANE_CULLING; + if (!smallFeatureCulling) + cullingMode &= ~osg::CullStack::SMALL_FEATURE_CULLING; + else + cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; + + // Configure eyes, their cameras, and their enslavement. + osg::Vec4 clearColor = mainCamera->getClearColor(); auto config = xr->getRecommendedSwapchainConfig(); - auto leftView = new VRView("LeftEye", config[(int)Side::LEFT_SIDE], context->getState()); - auto rightView = new VRView("RightEye", config[(int)Side::RIGHT_SIDE], context->getState()); - - mViews["LeftEye"] = leftView; - mViews["RightEye"] = rightView; - - auto leftCamera = mCameras["LeftEye"] = leftView->createCamera(0, clearColor, context); - auto rightCamera = mCameras["RightEye"] = rightView->createCamera(1, clearColor, context); - - leftCamera->setPreDrawCallback(mPreDraw); - rightCamera->setPreDrawCallback(mPreDraw); - - leftCamera->setFinalDrawCallback(mPostDraw); - rightCamera->setFinalDrawCallback(mPostDraw); - - // Stereo cameras should only draw the scene - leftCamera->setCullMask(~MWRender::Mask_GUI & ~MWRender::Mask_SimpleWater & ~MWRender::Mask_UpdateVisitor); - rightCamera->setCullMask(~MWRender::Mask_GUI & ~MWRender::Mask_SimpleWater & ~MWRender::Mask_UpdateVisitor); - - leftCamera->setName("LeftEye"); - rightCamera->setName("RightEye"); - - - osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING|osg::Camera::FAR_PLANE_CULLING; - - if (!Settings::Manager::getBool("small feature culling", "Camera")) - cullingMode &= ~(osg::CullStack::SMALL_FEATURE_CULLING); - else + for (unsigned i = 0; i < sViewNames.size(); i++) { - auto smallFeatureCullingPixelSize = Settings::Manager::getFloat("small feature culling pixel size", "Camera"); - leftCamera->setSmallFeatureCullingPixelSize(smallFeatureCullingPixelSize); - rightCamera->setSmallFeatureCullingPixelSize(smallFeatureCullingPixelSize); - cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; + auto view = new VRView(sViewNames[i], config[i], context->getState()); + mViews[sViewNames[i]] = view; + auto camera = mCameras[sViewNames[i]] = view->createCamera(i, clearColor, context); + camera->setPreDrawCallback(mPreDraw); + camera->setFinalDrawCallback(mPostDraw); + camera->setCullMask(~MWRender::Mask_GUI & ~MWRender::Mask_SimpleWater & ~MWRender::Mask_UpdateVisitor); + camera->setName(sViewNames[i]); + if (smallFeatureCulling) + camera->setSmallFeatureCullingPixelSize(smallFeatureCullingPixelSize); + camera->setCullingMode(cullingMode); + mViewer->addSlave(camera, true); + mViewer->getSlave(i)._updateSlaveCallback = new VRView::UpdateSlaveCallback(view, context); + + mMsaaResolveMirrorTexture[i].reset(new VRTexture(context->getState(), + view->swapchain().width(), + view->swapchain().height(), + 0)); } - leftCamera->setCullingMode(cullingMode); - rightCamera->setCullingMode(cullingMode); - mViewer->addSlave(leftCamera, true); - mViewer->addSlave(rightCamera, true); - - mViewer->setReleaseContextAtEndOfFrameHint(false); mMirrorTexture.reset(new VRTexture(context->getState(), mainCamera->getViewport()->width(), mainCamera->getViewport()->height(), 0)); - mMsaaResolveMirrorTexture[(int)Side::LEFT_SIDE].reset(new VRTexture(context->getState(), - leftView->swapchain().width(), - leftView->swapchain().height(), - 0)); - mMsaaResolveMirrorTexture[(int)Side::RIGHT_SIDE].reset(new VRTexture(context->getState(), - rightView->swapchain().width(), - rightView->swapchain().height(), - 0)); - - mViewer->getSlave(0)._updateSlaveCallback = new VRView::UpdateSlaveCallback(leftView, context); - mViewer->getSlave(1)._updateSlaveCallback = new VRView::UpdateSlaveCallback(rightView, context); + mViewer->setReleaseContextAtEndOfFrameHint(false); mMainCameraGC = mainCamera->getGraphicsContext(); mMainCameraGC->setSwapCallback(new VRViewer::SwapBuffersCallback(this)); @@ -132,6 +104,14 @@ namespace MWVR Log(Debug::Verbose) << "Realized"; } + VRView* VRViewer::getView(std::string name) + { + auto it = mViews.find(name); + if (it != mViews.end()) + return it->second.get(); + return nullptr; + } + void VRViewer::enableMainCamera(void) { mCameras["MainCamera"]->setGraphicsContext(mMainCameraGC); @@ -150,18 +130,16 @@ namespace MWVR int screenWidth = mCameras["MainCamera"]->getViewport()->width(); int mirrorWidth = screenWidth / 2; int screenHeight = mCameras["MainCamera"]->getViewport()->height(); - const char* viewNames[] = { - "RightEye", - "LeftEye" - }; - for (int i = 0; i < 2; i++) + for (unsigned i = 0; i < sViewNames.size(); i++) { auto& resolveTexture = *mMsaaResolveMirrorTexture[i]; resolveTexture.beginFrame(gc); - mViews[viewNames[i]]->swapchain().renderBuffer()->blit(gc, 0, 0, resolveTexture.width(), resolveTexture.height()); + mViews[sViewNames[i]]->swapchain().renderBuffer()->blit(gc, 0, 0, resolveTexture.width(), resolveTexture.height()); mMirrorTexture->beginFrame(gc); - resolveTexture.blit(gc, i * mirrorWidth, 0, (i + 1) * mirrorWidth, screenHeight); + // Mirror the index when rendering to the mirror texture to allow cross eye mirror textures. + unsigned mirrorIndex = sViewNames.size() - 1 - i; + resolveTexture.blit(gc, mirrorIndex * mirrorWidth, 0, (mirrorIndex + 1) * mirrorWidth, screenHeight); } gl->glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0); @@ -206,7 +184,8 @@ namespace MWVR view->postrenderCallback(info); - // OSG will sometimes overwrite the predraw callback. + // This happens sometimes, i've not been able to catch it when as happens + // to see why and how i can stop it. if (camera->getPreDrawCallback() != mPreDraw) { camera->setPreDrawCallback(mPreDraw); diff --git a/apps/openmw/mwvr/vrviewer.hpp b/apps/openmw/mwvr/vrviewer.hpp index c35c732ef..627b8aad2 100644 --- a/apps/openmw/mwvr/vrviewer.hpp +++ b/apps/openmw/mwvr/vrviewer.hpp @@ -14,6 +14,10 @@ namespace MWVR { + /// \brief Manages stereo rendering and mirror texturing. + /// + /// Manipulates the osgViewer by disabling main camera rendering, and instead rendering to + /// two slave cameras, each connected to and manipulated by a VRView class. class VRViewer { public: @@ -65,25 +69,27 @@ namespace MWVR VRViewer* mViewer; }; + + static const std::array sViewNames; + public: VRViewer( osg::ref_ptr viewer); ~VRViewer(void); - const XrCompositionLayerBaseHeader* layer(); - void traversals(); void preDrawCallback(osg::RenderInfo& info); void postDrawCallback(osg::RenderInfo& info); void blitEyesToMirrorTexture(osg::GraphicsContext* gc); void realize(osg::GraphicsContext* gc); bool realized() { return mConfigured; } + VRView* getView(std::string name); void enableMainCamera(void); void disableMainCamera(void); - public: + private: osg::ref_ptr mRealizeOperation = nullptr; osg::ref_ptr mViewer = nullptr; std::map > mViews{}; @@ -91,7 +97,6 @@ namespace MWVR osg::ref_ptr mPreDraw{ nullptr }; osg::ref_ptr mPostDraw{ nullptr }; osg::GraphicsContext* mMainCameraGC{ nullptr }; - //std::unique_ptr mMirrorTexture{ nullptr }; std::unique_ptr mMsaaResolveMirrorTexture[2]{ }; std::unique_ptr mMirrorTexture{ nullptr }; diff --git a/cmake/FindOpenXR.cmake b/cmake/FindOpenXR.cmake index 7a864e0fa..ece70d70d 100644 --- a/cmake/FindOpenXR.cmake +++ b/cmake/FindOpenXR.cmake @@ -6,22 +6,6 @@ # OpenXR_INCLUDE_DIR, where to find openxr.h # OpenXR_VERSION, the version of the found library # -# This module accepts the following env variables -# OPENXR_ROOT -# This module responds to the the flag: - -#============================================================================= -# Copyright 2003-2009 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) if(WIN32)