diff --git a/.gitignore b/.gitignore index 4cf92b549..d3ea1024a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ prebuilt ##windows build process /deps /MSVC* +.vs ## doxygen Doxygen diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ceba2841f..419b62bc5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -163,7 +163,7 @@ macOS10.15_Xcode11: CCACHE_SIZE: 3G variables: &engine-targets - targets: "openmw,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" + targets: "openmw_vr,openmw-essimporter,openmw-iniimporter,openmw-launcher,openmw-wizard" package: "Engine" variables: &cs-targets @@ -378,37 +378,37 @@ Windows_MSBuild_Tests_RelWithDebInfo: # Gitlab can't successfully execute following binaries due to unknown reason # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe" -Debian_AndroidNDK_arm64-v8a: - tags: - - linux - image: debian:bullseye - variables: - CCACHE_SIZE: 3G - cache: - key: Debian_AndroidNDK_arm64-v8a.v3 - paths: - - apt-cache/ - - ccache/ - - build/extern/fetched/ - before_script: - - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - - echo "deb http://deb.debian.org/debian unstable main contrib" > /etc/apt/sources.list - - echo "google-android-ndk-installer google-android-installers/mirror select https://dl.google.com" | debconf-set-selections - - apt-get update -yq - - apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer - stage: build - script: - - export CCACHE_BASEDIR="`pwd`" - - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" - - ccache -z -M "${CCACHE_SIZE}" - - CI/before_install.android.sh - - CI/before_script.android.sh - - cd build - - cmake --build . -- -j $(nproc) - - cmake --install . - - ccache -s - artifacts: - paths: - - build/install/ - # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. - timeout: 1h30m +#Debian_AndroidNDK_arm64-v8a: +# tags: +# - linux +# image: debian:bullseye +# variables: +# CCACHE_SIZE: 3G +# cache: +# key: Debian_AndroidNDK_arm64-v8a.v3 +# paths: +# - apt-cache/ +# - ccache/ +# - build/extern/fetched/ +# before_script: +# - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR +# - echo "deb http://deb.debian.org/debian unstable main contrib" > /etc/apt/sources.list +# - echo "google-android-ndk-installer google-android-installers/mirror select https://dl.google.com" | debconf-set-selections +# - apt-get update -yq +# - apt-get -q -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake ccache curl unzip git build-essential google-android-ndk-installer +# stage: build +# script: +# - export CCACHE_BASEDIR="`pwd`" +# - export CCACHE_DIR="`pwd`/ccache" && mkdir -pv "$CCACHE_DIR" +# - ccache -z -M "${CCACHE_SIZE}" +# - CI/before_install.android.sh +# - CI/before_script.android.sh +# - cd build +# - cmake --build . -- -j $(nproc) +# - cmake --install . +# - ccache -s +# artifacts: +# paths: +# - build/install/ +# # When CCache doesn't exist (e.g. first build on a fork), build takes more than 1h, which is the default for forks. +# timeout: 1h30m diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh old mode 100644 new mode 100755 index bb662c9de..1cf510e34 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -93,6 +93,9 @@ while [ $# -gt 0 ]; do case $ARG in V ) VERBOSE=true ;; + + nVR ) + SKIP_VR=true ;; d ) SKIP_DOWNLOAD=true ;; @@ -555,14 +558,14 @@ if [ -z $SKIP_DOWNLOAD ]; then "OpenAL-Soft-1.20.1.zip" # OSG - download "OpenSceneGraph 3.6.5" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ - "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" + download "OpenSceneGraph 3.6.x" \ + "https://gitlab.com/madsbuvi/openmw-deps/-/raw/openmw-vr/windows/OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ + "OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" if [ -n "$PDBS" ]; then download "OpenSceneGraph symbols" \ - "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ - "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" + "https://gitlab.com/madsbuvi/openmw-deps/-/raw/openmw-vr/windows/OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ + "OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" fi # SDL2 @@ -760,7 +763,7 @@ printf "OpenAL-Soft 1.20.1... " cd $DEPS echo # OSG -printf "OSG 3.6.5... " +printf "OSG 3.6.x... " { cd $DEPS_INSTALL if [ -d OSG ] && \ @@ -771,9 +774,9 @@ printf "OSG 3.6.5... " printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf OSG - eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP - [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP - mv "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}" OSG + eval 7z x -y "${DEPS}/OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" $STRIP + [ -n "$PDBS" ] && eval 7z x -y "${DEPS}/OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" $STRIP + mv "OSG-3.6.x-msvc${MSVC_REAL_YEAR}-win${BITS}" OSG fi OSG_SDK="$(real_pwd)/OSG" add_cmake_opts -DOSG_DIR="$OSG_SDK" @@ -783,7 +786,7 @@ printf "OSG 3.6.5... " else SUFFIX="" fi - add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ + add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng16}${SUFFIX}.dll \ "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll @@ -934,6 +937,7 @@ printf "LZ4 1.9.2... " } cd $DEPS echo + # Google Test and Google Mock if [ ! -z $TEST_FRAMEWORK ]; then printf "Google test 1.10.0 ..." @@ -988,6 +992,11 @@ if [ ! -z $TEST_FRAMEWORK ]; then fi +# VR build +if [ ! -Z $SKIP_VR ]; then + add_cmake_opts -DBUILD_VR_OPENXR=no +fi + echo cd $DEPS_INSTALL/.. echo @@ -1003,6 +1012,7 @@ if [ ! -z $CI ]; then -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENCS=no \ -DBUILD_OPENMW=no \ + -DBUILD_OPENMW_VR=no \ -DBUILD_WIZARD=no ;; openmw ) @@ -1011,20 +1021,31 @@ if [ ! -z $CI ]; then -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENCS=no \ + -DBUILD_OPENMW_VR=no \ -DBUILD_WIZARD=no ;; + vr ) + echo " Building subproject: OpenMW-VR." + add_cmake_opts -DBUILD_ESSIMPORTER=no \ + -DBUILD_OPENCS=no \ + -DBUILD_BSATOOL=no \ + -DBUILD_OPENMW=no \ + -DBUILD_ESMTOOL=no + ;; opencs ) echo " Building subproject: OpenCS." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENMW=no \ + -DBUILD_OPENMW_VR=no \ -DBUILD_WIZARD=no ;; misc ) echo " Building subprojects: Misc." add_cmake_opts -DBUILD_OPENCS=no \ - -DBUILD_OPENMW=no + -DBUILD_OPENMW=no \ + -DBUILD_OPENMW_VR=no ;; esac fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 59d81eb9b..ece3f91af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ option(BUILD_BSATOOL "Build BSA extractor" ON) option(BUILD_ESMTOOL "Build ESM inspector" ON) option(BUILD_NIFTEST "Build nif file tester" ON) option(BUILD_DOCS "Build documentation." OFF ) +option(BUILD_OPENMW_VR "Build VR support using OpenXR" ON) option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF) option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF) option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF) @@ -215,7 +216,7 @@ endif() # Start of tes3mp addition # # Don't require certain dependencies for the server -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition set(USED_OSG_COMPONENTS osgDB @@ -239,7 +240,7 @@ set(USED_OSG_PLUGINS # Start of tes3mp addition # # Don't require certain dependencies for the server -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition add_subdirectory(extern) @@ -249,7 +250,7 @@ add_subdirectory(extern) # Start of tes3mp addition # # Don't require certain dependencies for the server -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition # Require at least ffmpeg 3.2 for now SET(FFVER_OK FALSE) @@ -312,7 +313,7 @@ endif() # Start of tes3mp addition # # Don't require certain dependencies for the server -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition # Platform specific @@ -332,7 +333,7 @@ endif() # Start of tes3mp addition # # Don't require certain dependencies for the server -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition if(OPENMW_USE_SYSTEM_BULLET) set(REQUIRED_BULLET_VERSION 286) # Bullet 286 required due to runtime bugfixes for btCapsuleShape @@ -369,7 +370,7 @@ endif() # Start of tes3mp addition # # Don't require certain dependencies for the server -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition if (NOT WIN32 AND BUILD_WIZARD) # windows users can just run the morrowind installer @@ -394,10 +395,12 @@ endif() # # Don't require certain dependencies for the server # but keep OSG's headers (HACK) -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition + if(OPENMW_USE_SYSTEM_OSG) - find_package(OpenSceneGraph 3.4.0 REQUIRED ${USED_OSG_COMPONENTS}) + find_package(OpenSceneGraph ${OSG_VERSION_REQUIRED} REQUIRED ${USED_OSG_COMPONENTS}) + if (${OPENSCENEGRAPH_VERSION} VERSION_GREATER 3.6.2 AND ${OPENSCENEGRAPH_VERSION} VERSION_LESS 3.6.5) message(FATAL_ERROR "OpenSceneGraph version ${OPENSCENEGRAPH_VERSION} has critical regressions which cause crashes. Please upgrade to 3.6.5 or later. We strongly recommend using the tip of the official 'OpenSceneGraph-3.6' branch or the tip of '3.6' OpenMW/osg (OSGoS).") endif() @@ -416,9 +419,9 @@ endif() # # Don't require certain dependencies for the server # but keep OSG's headers (HACK) -ELSE(BUILD_OPENMW OR BUILD_OPENCS) +ELSE(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) include_directories(${OPENSCENEGRAPH_INCLUDE_DIRS}) -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition set(BOOST_COMPONENTS system filesystem program_options iostreams) @@ -441,7 +444,7 @@ find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPON # Start of tes3mp addition # # Don't require certain dependencies for the server -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition if(OPENMW_USE_SYSTEM_MYGUI) find_package(MyGUI 3.2.2 REQUIRED) @@ -449,7 +452,7 @@ endif() # End of tes3mp addition # # Don't require certain dependencies for the server -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition find_package(SDL2 2.0.9 REQUIRED) # Start of tes3mp addition @@ -521,6 +524,9 @@ configure_resource_file(${OpenMW_SOURCE_DIR}/files/tes3mp/tes3mp-server-default. pack_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg "${OpenMW_BINARY_DIR}" "defaults.bin") +configure_resource_file(${OpenMW_SOURCE_DIR}/files/settings-overrides-vr.cfg + "${OpenMW_BINARY_DIR}" "settings-overrides-vr.cfg") + configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}" "openmw.appdata.xml") @@ -598,20 +604,20 @@ endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clan # Start of tes3mp addition # # Don't require certain dependencies for the server -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition add_subdirectory (extern/osg-ffmpeg-videoplayer) add_subdirectory (extern/oics) # Start of tes3mp addition # # Don't require certain dependencies for the server -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition add_subdirectory (extern/Base64) # Start of tes3mp addition # # Don't require certain dependencies for the server -IF(BUILD_OPENMW OR BUILD_OPENCS) +IF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition if (BUILD_OPENCS) add_subdirectory (extern/osgQt) @@ -619,7 +625,7 @@ endif() # Start of tes3mp addition # # Don't require certain dependencies for the server -ENDIF(BUILD_OPENMW OR BUILD_OPENCS) +ENDIF(BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp addition # Components @@ -635,7 +641,7 @@ if (BUILD_MASTER) add_subdirectory( apps/master ) endif() -if (BUILD_OPENMW) +if (BUILD_OPENMW OR BUILD_OPENMW_VR) add_subdirectory( apps/openmw ) endif() @@ -712,6 +718,23 @@ if (WIN32) set_target_properties(tes3mp PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() + # TODO: properties and link targets should be copied from openmw to openmw_vr instead of duplicating every line + if (USE_DEBUG_CONSOLE AND BUILD_OPENMW_VR) + set_target_properties(openmw_vr PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE") + set_target_properties(openmw_vr PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE") + set_target_properties(openmw_vr PROPERTIES COMPILE_DEFINITIONS $<$:_CONSOLE>) + elseif (BUILD_OPENMW_VR) + # Turn off debug console, debug output will be written to visual studio output instead + set_target_properties(openmw_vr PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:WINDOWS") + set_target_properties(openmw_vr PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:WINDOWS") + endif() + + if (BUILD_OPENMW_VR) + # Release builds don't use the debug console + set_target_properties(openmw_vr PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") + set_target_properties(openmw_vr PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") + endif() + # Play a bit with the warning levels set(WARNINGS "/W4") @@ -784,6 +807,14 @@ if (WIN32) endif() endif() + if (BUILD_OPENMW_VR) + if (OPENMW_UNITY_BUILD) + set_target_properties(openmw_vr PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD} /bigobj") + else() + set_target_properties(openmw_vr PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") + endif() + endif() + if (BUILD_WIZARD) set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS} ${MT_BUILD}") endif() @@ -936,6 +967,7 @@ elseif(NOT APPLE) INSTALL(FILES "${INSTALL_SOURCE}/tes3mp-server-default.cfg" DESTINATION ".") # End of tes3mp addition INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".") + INSTALL(FILES "${INSTALL_SOURCE}/xrcontrollersuggestions.xml" DESTINATION ".") INSTALL(DIRECTORY "${INSTALL_SOURCE}/resources" DESTINATION ".") @@ -1076,6 +1108,7 @@ elseif(NOT APPLE) INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") + INSTALL(FILES "${INSTALL_SOURCE}/xrcontrollersuggestions.xml" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") # Start of tes3mp addition INSTALL(FILES "${INSTALL_SOURCE}/tes3mp-client-default.cfg" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") diff --git a/LICENSE b/LICENSE index 719dc8c05..bc6d32273 100644 --- a/LICENSE +++ b/LICENSE @@ -679,3 +679,9 @@ AND AUTHORS OF THAT COVERED WORK FOR ANY DAMAEGS, DEMANDS, CLAIMS, LOSSES, CAUSES OF ACTION, LAWSUITS, JUDGMENTS EXPENSES (INCLUDING WITHOUT LIMITATION REASONABLE ATTORNEYS' FEES AND EXPENSES) OR ANY OTHER LIABLITY ARISING FROM, RELATED TO OR IN CONNECTION WITH YOUR ASSUMPTIONS OF LIABILITY. + +----------------------------------------------------------------------------- +----------------------------------------------------------------------------- +On Windows releases, this project bundles the OpenXR loader binary, which is +licensed under the Apache 2.0 license. For license details, see: +https://github.com/KhronosGroup/OpenXR-SDK/blob/master/LICENSE diff --git a/README.md b/README.md index 92b5934b6..06b36dc20 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ -TES3MP -====== +TES3MP VR +========= Copyright (c) 2008-2015, OpenMW Team Copyright (c) 2016-2022, David Cernat & Stanislav Zhukov TES3MP is a project adding multiplayer functionality to [OpenMW](https://github.com/OpenMW/openmw), an open-source game engine that supports playing "The Elder Scrolls III: Morrowind" by Bethesda Softworks. +When it's combined with Mads Buvik Sandvei's [fork adding VR support to OpenMW](https://gitlab.com/madsbuvi/openmw), the result is TES3MP VR. + * TES3MP version: 0.8.0 * OpenMW version: 0.47.0 * License: GPLv3 (see [LICENSE](https://github.com/TES3MP/TES3MP/blob/master/LICENSE) for more information) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index bddb70aa0..d9f24d931 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -230,6 +230,26 @@ bool Launcher::AdvancedPage::loadSettings() startDefaultCharacterAtField->setText(mGameSettings.value("start")); runScriptAfterStartupField->setText(mGameSettings.value("script-run")); } + + // VR + { + std::string stereoMethod = Settings::Manager::getString("stereo method", "Stereo"); + useGeometryShaders->setChecked(stereoMethod == "GeometryShader"); + + loadSettingBool(useSharedShadowMaps, "shared shadow maps", "Stereo"); + loadSettingBool(preferDirectXSwapchains, "Prefer DirectX swapchains", "VR"); + loadSettingBool(preferSRGBSwapchains, "Prefer sRGB swapchains", "VR"); + loadSettingBool(useXrDebug, "enable XR_EXT_debug_utils", "VR Debug"); + loadSettingBool(logAllXrCalls, "log all openxr calls", "VR Debug"); + loadSettingBool(ignoreXrErrors, "continue on errors", "VR Debug"); + + double minimumSwingSpeed = Settings::Manager::getDouble("realistic combat minimum swing velocity", "VR"); + realisticCombatMinimumSwingSpeedSpinBox->setValue(minimumSwingSpeed); + double maximumSwingSpeed = Settings::Manager::getDouble("realistic combat maximum swing velocity", "VR"); + realisticCombatMaximumSwingSpeedSpinBox->setValue(maximumSwingSpeed); + double realHeightValue = Settings::Manager::getDouble("real height", "VR"); + realHeightSpinBox->setValue(realHeightValue); + } return true; } @@ -395,6 +415,34 @@ void Launcher::AdvancedPage::saveSettings() if (scriptRun != mGameSettings.value("script-run")) mGameSettings.setValue("script-run", scriptRun); } + + // VR + { + std::string stereoMethod = "BruteForce"; + if (useGeometryShaders->isChecked()) + stereoMethod = "GeometryShader"; + if (stereoMethod != Settings::Manager::getString("stereo method", "Stereo")) + Settings::Manager::setString("stereo method", "Stereo", stereoMethod); + + saveSettingBool(useSharedShadowMaps, "shared shadow maps", "Stereo"); + saveSettingBool(preferDirectXSwapchains, "Prefer sRGB swapchains", "VR"); + saveSettingBool(preferSRGBSwapchains, "Prefer DirectX swapchains", "VR"); + saveSettingBool(useXrDebug, "enable XR_EXT_debug_utils", "VR Debug"); + saveSettingBool(logAllXrCalls, "log all openxr calls", "VR Debug"); + saveSettingBool(ignoreXrErrors, "continue on errors", "VR Debug"); + + double minimumSwingSpeed = realisticCombatMinimumSwingSpeedSpinBox->value(); + if (minimumSwingSpeed != Settings::Manager::getFloat("realistic combat minimum swing velocity", "VR")) + Settings::Manager::setFloat("realistic combat minimum swing velocity", "VR", minimumSwingSpeed); + + double maximumSwingSpeed = realisticCombatMaximumSwingSpeedSpinBox->value(); + if (maximumSwingSpeed != Settings::Manager::getFloat("realistic combat maximum swing velocity", "VR")) + Settings::Manager::setFloat("realistic combat maximum swing velocity", "VR", maximumSwingSpeed); + + double realHeightValue = realHeightSpinBox->value(); + if (realHeightValue != Settings::Manager::getFloat("real height", "VR")) + Settings::Manager::setFloat("real height", "VR", realHeightValue); + } } void Launcher::AdvancedPage::loadSettingBool(QCheckBox *checkbox, const std::string &setting, const std::string &group) diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index dbed1ba97..c444cc8c6 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "../widget/scenetoolmode.hpp" @@ -104,7 +105,7 @@ RenderWidget::~RenderWidget() // before OSG 3.6.4, the default font was a static object, and if it wasn't attached to the scene when a graphics context was destroyed, it's program wouldn't be released. // 3.6.4 moved it into the object cache, which meant it usually got released, but not here. // 3.6.5 improved cleanup with osgViewer::CompositeViewer::removeView so it more reliably released associated state for objects in the object cache. - osg::ref_ptr graphicsContext = mView->getCamera()->getGraphicsContext(); + osg::ref_ptr graphicsContext = SDLUtil::GraphicsWindowSDL2::findContext(*mView); osgText::Font::getDefaultFont()->releaseGLObjects(graphicsContext->getState()); #endif } @@ -132,7 +133,7 @@ osg::Camera *RenderWidget::getCamera() void RenderWidget::toggleRenderStats() { osgViewer::GraphicsWindow* window = - static_cast(mView->getCamera()->getGraphicsContext()); + static_cast(SDLUtil::GraphicsWindowSDL2::findContext(*mView)); window->getEventQueue()->keyPress(osgGA::GUIEventAdapter::KEY_S); window->getEventQueue()->keyRelease(osgGA::GUIEventAdapter::KEY_S); @@ -263,7 +264,7 @@ SceneWidget::SceneWidget(std::shared_ptr resourceSyste SceneWidget::~SceneWidget() { // Since we're holding on to the resources past the existence of this graphics context, we'll need to manually release the created objects - mResourceSystem->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState()); + mResourceSystem->releaseGLObjects(SDLUtil::GraphicsWindowSDL2::findContext(*mView)->getState()); } diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index f81885308..dd327eca6 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -170,7 +170,7 @@ include_directories( ${FFmpeg_INCLUDE_DIRS} ) -target_link_libraries(tes3mp +set(OPENMW_LINK_TARGETS # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`. @@ -198,6 +198,16 @@ target_link_libraries(tes3mp ${RakNet_LIBRARY} ) +if (USE_SYSTEM_TINYXML) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + ${TinyXML_LIBRARIES}) +endif() + +if (NOT UNIX) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + ${SDL2MAIN_LIBRARY}) +endif() + if(OSG_STATIC) unset(_osg_plugins_static_files) add_library(openmw_osg_plugins INTERFACE) @@ -214,30 +224,36 @@ if(OSG_STATIC) # We use --whole-archive because OSG plugins use registration. get_whole_archive_options(_opts ${_osg_plugins_static_files}) target_link_options(openmw_osg_plugins INTERFACE ${_opts}) - target_link_libraries(tes3mp openmw_osg_plugins) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + openmw_osg_plugins) if(OPENMW_USE_SYSTEM_OSG) # OSG plugin pkgconfig files are missing these dependencies. # https://github.com/openscenegraph/OpenSceneGraph/issues/1052 - target_link_libraries(tes3mp freetype jpeg png) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + freetype jpeg png) endif() endif(OSG_STATIC) if (ANDROID) - target_link_libraries(tes3mp EGL android log z) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + EGL android log z) endif (ANDROID) if (USE_SYSTEM_TINYXML) - target_link_libraries(tes3mp ${TinyXML_LIBRARIES}) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + ${TinyXML_LIBRARIES}) endif() if (NOT UNIX) -target_link_libraries(tes3mp ${SDL2MAIN_LIBRARY}) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + ${SDL2MAIN_LIBRARY}) endif() # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) -target_link_libraries(tes3mp ${CMAKE_THREAD_LIBS_INIT}) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + ${CMAKE_THREAD_LIBS_INIT}) endif() if(APPLE) @@ -252,24 +268,26 @@ if(APPLE) configure_file("${OpenMW_BINARY_DIR}/openmw.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY) - add_custom_command(TARGET openmw - POST_BUILD - COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${BUNDLE_RESOURCES_DIR}/resources") - find_library(COCOA_FRAMEWORK Cocoa) find_library(IOKIT_FRAMEWORK IOKit) - target_link_libraries(tes3mp ${COCOA_FRAMEWORK} ${IOKIT_FRAMEWORK}) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + ${COCOA_FRAMEWORK} + ${IOKIT_FRAMEWORK}) if (FFmpeg_FOUND) find_library(COREVIDEO_FRAMEWORK CoreVideo) find_library(VDA_FRAMEWORK VideoDecodeAcceleration) - target_link_libraries(tes3mp z ${COREVIDEO_FRAMEWORK} ${VDA_FRAMEWORK}) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + z + ${COREVIDEO_FRAMEWORK} + ${VDA_FRAMEWORK}) endif() endif(APPLE) if (BUILD_WITH_CODE_COVERAGE) add_definitions (--coverage) - target_link_libraries(tes3mp gcov) + set(OPENMW_LINK_TARGETS ${OPENMW_LINK_TARGETS} + gcov) endif() if (MSVC) @@ -278,6 +296,95 @@ if (MSVC) set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /bigobj") endif (CMAKE_CL_64) endif (MSVC) + +if(BUILD_OPENMW) + if (NOT ANDROID) + openmw_add_executable(openmw + ${OPENMW_FILES} + ${GAME} ${GAME_HEADER} + ${APPLE_BUNDLE_RESOURCES} + ) + else () + add_library(openmw + SHARED + ${OPENMW_FILES} + ${GAME} ${GAME_HEADER} + ) + endif () + + target_link_libraries(tes3mp ${OPENMW_LINK_TARGETS}) + + if(APPLE) + add_custom_command(TARGET tes3mp + POST_BUILD + COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${BUNDLE_RESOURCES_DIR}/resources") + endif(APPLE) + + if (WIN32) + INSTALL(TARGETS openmw RUNTIME DESTINATION ".") + endif (WIN32) +endif() + +if(BUILD_OPENMW_VR) +# Use of FetchContent to include the OpenXR SDK requires CMake 3.11 + if(${CMAKE_VERSION} VERSION_LESS "3.11") + message(FATAL_ERROR "Building openmw_vr requires CMake version 3.11 or later.") + endif() + +# TODO: 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 + ) + add_openmw_dir (mwvr + openxraction openxractionset openxrdebug openxrinput openxrmanager openxrmanagerimpl openxrplatform openxrswapchain openxrswapchainimage openxrswapchainimpl openxrtracker openxrtypeconversions + realisticcombat + vranimation vrcamera vrenvironment vrframebuffer vrgui vrinputmanager vrinput vrlistbox vrmetamenu vrpointer vrsession vrtracking vrtypes vrutil vrviewer vrvirtualkeyboard + ) + + openmw_add_executable(openmw_vr + ${OPENMW_FILES} + ${OPENMW_VR_FILES} + ${GAME} ${GAME_HEADER} + ${APPLE_BUNDLE_RESOURCES} + ) + + configure_resource_file(${OpenMW_SOURCE_DIR}/files/xrcontrollersuggestions.xml + "${OpenMW_BINARY_DIR}" "xrcontrollersuggestions.xml") + + ########### Import the OpenXR SDK + # Force the openxr-sdk to use its bundled jsoncpp to avoid problems from system jsoncpp if present + set(BUILD_WITH_SYSTEM_JSONCPP off) + include(FetchContent) + FetchContent_Declare( + OpenXR + GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git + GIT_TAG release-1.0.15 + ) + FetchContent_MakeAvailable(OpenXR) + + target_link_libraries(openmw_vr openxr_loader) + + # Preprocessor variable used to control code paths to vr code + if (WIN32) + target_compile_options(openmw_vr PUBLIC -DUSE_OPENXR -DXR_USE_GRAPHICS_API_OPENGL -DXR_USE_GRAPHICS_API_D3D11 -DXR_USE_PLATFORM_WIN32) + elseif(UNIX) + target_compile_options(openmw_vr PUBLIC -DUSE_OPENXR -DXR_USE_GRAPHICS_API_OPENGL -DXR_USE_PLATFORM_XLIB) + find_package(X11 REQUIRED) + target_link_libraries(openmw_vr ${X11_LIBRARIES}) + endif() + target_link_libraries(openmw_vr ${OPENMW_LINK_TARGETS}) + if(APPLE) + add_custom_command(TARGET openmw_vr + POST_BUILD + COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${BUNDLE_RESOURCES_DIR}/resources") + endif(APPLE) + + if (WIN32) + INSTALL(TARGETS openmw_vr RUNTIME DESTINATION ".") + endif (WIN32) +endif() if (WIN32) INSTALL(TARGETS tes3mp RUNTIME DESTINATION ".") diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index ffae131ca..7eb22d30c 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -24,12 +24,17 @@ #include #include +#include + #include #include #include #include +#include +#include + #include #include @@ -66,6 +71,7 @@ #include "mwworld/worldimp.hpp" #include "mwrender/vismask.hpp" +#include "mwrender/camera.hpp" #include "mwclass/classes.hpp" @@ -77,6 +83,13 @@ #include "mwstate/statemanagerimp.hpp" +#ifdef USE_OPENXR +#include "mwvr/vrinputmanager.hpp" +#include "mwvr/vrviewer.hpp" +#include "mwvr/vrgui.hpp" +#include "mwvr/vrcamera.hpp" +#endif + namespace { void checkSDLError(int ret) @@ -465,6 +478,9 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) , mEncoding(ToUTF8::WINDOWS_1252) , mEncoder(nullptr) , mScreenCaptureOperation(nullptr) + , mStereoEnabled(false) + , mStereoOverride(false) + , mStereoView(nullptr) , mSkipMenu (false) , mUseSound (true) , mCompileAll (false) @@ -508,6 +524,8 @@ OMW::Engine::~Engine() End of tes3mp addition */ + mStereoView = nullptr; + mEnvironment.cleanup(); /* @@ -617,6 +635,18 @@ std::string OMW::Engine::loadSettings (Settings::Manager & settings) if (boost::filesystem::exists(settingspath)) settings.loadUser(settingspath); + +#ifdef USE_OPENXR + const std::string localoverrides = (mCfgMgr.getLocalPath() / "settings-overrides-vr.cfg").string(); + const std::string globaloverrides = (mCfgMgr.getGlobalPath() / "settings-overrides-vr.cfg").string(); + if (boost::filesystem::exists(localoverrides)) + settings.loadOverrides(localoverrides); + else if (boost::filesystem::exists(globaloverrides)) + settings.loadOverrides(globaloverrides); + else + throw std::runtime_error("No settings overrides file found! Make sure the file \"settings-overrides-vr.cfg\" was properly installed."); +#endif + return settingspath; } @@ -734,6 +764,10 @@ void OMW::Engine::createWindow(Settings::Manager& settings) if (Debug::shouldDebugOpenGL()) mViewer->setRealizeOperation(new Debug::EnableGLDebugOperation()); +#ifdef USE_OPENXR + initVr(); +#endif + mViewer->realize(); mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); @@ -773,6 +807,8 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) osg::ref_ptr rootNode (new osg::Group); mViewer->setSceneData(rootNode); + mCallbackManager.reset(new Misc::CallbackManager(mViewer)); + mVFS.reset(new VFS::Manager(mFSStrict)); VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); @@ -785,6 +821,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) Settings::Manager::getString("texture mipmap", "General"), Settings::Manager::getInt("anisotropy", "General") ); + mEnvironment.setResourceSystem(mResourceSystem.get()); int numThreads = Settings::Manager::getInt("preload num threads", "Cells"); if (numThreads <= 0) @@ -838,25 +875,89 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string()); mEnvironment.setWindowManager (window); - MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); +#ifdef USE_OPENXR + const std::string xrinputuserdefault = mCfgMgr.getUserConfigPath().string() + "/xrcontrollersuggestions.xml"; + const std::string xrinputlocaldefault = mCfgMgr.getLocalPath().string() + "/xrcontrollersuggestions.xml"; + const std::string xrinputglobaldefault = mCfgMgr.getGlobalPath().string() + "/xrcontrollersuggestions.xml"; + + std::string xrControllerSuggestions; + if (boost::filesystem::exists(xrinputuserdefault)) + xrControllerSuggestions = xrinputuserdefault; + else if (boost::filesystem::exists(xrinputlocaldefault)) + xrControllerSuggestions = xrinputlocaldefault; + else if (boost::filesystem::exists(xrinputglobaldefault)) + xrControllerSuggestions = xrinputglobaldefault; + else + xrControllerSuggestions = ""; //if it doesn't exist, pass in an empty string + + Log(Debug::Verbose) << "xrinputuserdefault: " << xrinputuserdefault; + Log(Debug::Verbose) << "xrinputlocaldefault: " << xrinputlocaldefault; + Log(Debug::Verbose) << "xrinputglobaldefault: " << xrinputglobaldefault; + + MWInput::InputManager* input = + new MWVR::VRInputManager(mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab, xrControllerSuggestions); +#else + MWInput::InputManager* input = + new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); +#endif mEnvironment.setInputManager (input); // Create sound system mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mUseSound)); + + if (mStereoEnabled) + { + // Set up stereo + // Stereo setup is split in two because the GeometryShader approach cannot be used before the RenderingManager has been created. + // To be able to see the logo and initial loading screen the BruteForce technique must be set up here. + mStereoView->initializeStereo(mViewer, Misc::StereoView::Technique::BruteForce); + mResourceSystem->getSceneManager()->getShaderManager().setStereoGeometryShaderEnabled(Misc::getStereoTechnique() == Misc::StereoView::Technique::GeometryShader_IndexedViewports); + } + +#ifdef USE_OPENXR + mXrEnvironment.setGUIManager(new MWVR::VRGUIManager(mViewer, mResourceSystem.get(), rootNode)); + mXrEnvironment.getViewer()->configureCallbacks(); + mStereoView->setCullMask(mStereoView->getCullMask() & ~MWRender::VisMask::Mask_GUI); +#endif + + +#ifdef USE_OPENXR + MWVR::VRCamera* camera = new MWVR::VRCamera(mViewer->getCamera()); +#else + MWRender::Camera* camera = new MWRender::Camera(mViewer->getCamera()); +#endif + + if (!mSkipMenu) { const std::string& logo = Fallback::Map::getString("Movies_Company_Logo"); if (!logo.empty()) - window->playVideo(logo, true); + mEnvironment.getWindowManager()->playVideo(logo, true); } // Create the world - mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(), + mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, std::unique_ptr(camera), mResourceSystem.get(), mWorkQueue.get(), mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder, mActivationDistanceOverride, mCellName, mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string())); mEnvironment.getWorld()->setupPlayer(); +#ifdef USE_OPENXR + // TODO: Workaround. Needed to stop camera from querying the world object before it is created. + // This will be prettier when i clean up the tracking logic. + camera->setShouldTrackPlayerCharacter(true); +#endif + + if (mStereoEnabled) + { + // Stereo shader technique can be set up now. + mStereoView->setStereoTechnique(Misc::getStereoTechnique()); + mStereoView->initializeScene(); + + if (mEnvironment.getVrMode()) + mStereoView->setCullMask(mStereoView->getCullMask() & ~MWRender::VisMask::Mask_GUI); + } + window->setStore(mEnvironment.getWorld()->getStore()); window->initUI(); @@ -881,7 +982,6 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) // Create dialog system mEnvironment.setJournal (new MWDialogue::Journal); mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mTranslationDataStorage)); - mEnvironment.setResourceSystem(mResourceSystem.get()); // scripts if (mCompileAll) @@ -985,6 +1085,22 @@ void OMW::Engine::go() // Create encoder mEncoder = new ToUTF8::Utf8Encoder(mEncoding); +#ifdef USE_OPENXR + mEnvironment.setVrMode(true); +#endif + + // geometry shader must be enabled before the RenderingManager sets up any shaders + // therefore this part is separate from the rest of stereo setup. + mStereoEnabled = mEnvironment.getVrMode() || Settings::Manager::getBool("stereo enabled", "Stereo"); + if (mStereoEnabled) + { + // Mask in everything that does not currently use shaders. + // Remove that altogether when the sky finally uses them. + auto noShaderMask = MWRender::VisMask::Mask_Sky | MWRender::VisMask::Mask_Sun | MWRender::VisMask::Mask_WeatherParticles; + // Since shaders are not yet created, we need to use the brute force technique initially + mStereoView.reset(new Misc::StereoView(noShaderMask, MWRender::VisMask::Mask_Scene)); + } + // Setup viewer mViewer = new osgViewer::Viewer; mViewer->setReleaseContextAtEndOfFrameHint(false); @@ -1093,12 +1209,7 @@ void OMW::Engine::go() } else { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - - mEnvironment.getWorld()->updateWindowManager(); - - mViewer->renderingTraversals(); + mEnvironment.getWindowManager()->viewerTraversals(true); bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); diff --git a/apps/openmw/engine.cpp.orig b/apps/openmw/engine.cpp.orig new file mode 100644 index 000000000..a96e3a38b --- /dev/null +++ b/apps/openmw/engine.cpp.orig @@ -0,0 +1,883 @@ +#include "engine.hpp" + +#include + +#include + +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include + +#include + +#include + +#include + +#include + +#include "mwinput/inputmanagerimp.hpp" + +#include "mwgui/windowmanagerimp.hpp" + +#include "mwscript/scriptmanagerimp.hpp" +#include "mwscript/interpretercontext.hpp" + +#include "mwsound/soundmanagerimp.hpp" + +#include "mwworld/class.hpp" +#include "mwworld/player.hpp" +#include "mwworld/worldimp.hpp" + +#include "mwrender/vismask.hpp" + +#include "mwclass/classes.hpp" + +#include "mwdialogue/dialoguemanagerimp.hpp" +#include "mwdialogue/journalimp.hpp" +#include "mwdialogue/scripttest.hpp" + +#include "mwmechanics/mechanicsmanagerimp.hpp" + +#include "mwstate/statemanagerimp.hpp" + +#ifdef USE_OPENXR +#include "mwvr/openxrinputmanager.hpp" +#include "mwvr/openxrviewer.hpp" +#endif + +namespace +{ + void checkSDLError(int ret) + { + if (ret != 0) + Log(Debug::Error) << "SDL error: " << SDL_GetError(); + } +} + +void OMW::Engine::executeLocalScripts() +{ + MWWorld::LocalScripts& localScripts = mEnvironment.getWorld()->getLocalScripts(); + + localScripts.startIteration(); + std::pair script; + while (localScripts.getNext(script)) + { + MWScript::InterpreterContext interpreterContext ( + &script.second.getRefData().getLocals(), script.second); + mEnvironment.getScriptManager()->run (script.first, interpreterContext); + } +} + +bool OMW::Engine::frame(float frametime) +{ + try + { + mStartTick = mViewer->getStartTick(); + + mEnvironment.setFrameDuration(frametime); + + // update input + mEnvironment.getInputManager()->update(frametime, false); + + // When the window is minimized, pause the game. Currently this *has* to be here to work around a MyGUI bug. + // If we are not currently rendering, then RenderItems will not be reused resulting in a memory leak upon changing widget textures (fixed in MyGUI 3.3.2), + // and destroyed widgets will not be deleted (not fixed yet, https://github.com/MyGUI/mygui/issues/21) + if (!mEnvironment.getWindowManager()->isWindowVisible()) + { + mEnvironment.getSoundManager()->pausePlayback(); + return false; + } + else + mEnvironment.getSoundManager()->resumePlayback(); + + // sound + if (mUseSound) + mEnvironment.getSoundManager()->update(frametime); + + // Main menu opened? Then scripts are also paused. + bool paused = mEnvironment.getWindowManager()->containsMode(MWGui::GM_MainMenu); + + // update game state + mEnvironment.getStateManager()->update (frametime); + + bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); + + osg::Timer_t beforeScriptTick = osg::Timer::instance()->tick(); + if (mEnvironment.getStateManager()->getState()!= + MWBase::StateManager::State_NoGame) + { + if (!paused) + { + if (mEnvironment.getWorld()->getScriptsEnabled()) + { + // local scripts + executeLocalScripts(); + + // global scripts + mEnvironment.getScriptManager()->getGlobalScripts().run(); + } + + mEnvironment.getWorld()->markCellAsUnchanged(); + } + + if (!guiActive) + { + double hours = (frametime * mEnvironment.getWorld()->getTimeScaleFactor()) / 3600.0; + mEnvironment.getWorld()->advanceTime(hours, true); + mEnvironment.getWorld()->rechargeItems(frametime, true); + } + } + osg::Timer_t afterScriptTick = osg::Timer::instance()->tick(); + + // update actors + osg::Timer_t beforeMechanicsTick = osg::Timer::instance()->tick(); + if (mEnvironment.getStateManager()->getState()!= + MWBase::StateManager::State_NoGame) + { + mEnvironment.getMechanicsManager()->update(frametime, + guiActive); + } + osg::Timer_t afterMechanicsTick = osg::Timer::instance()->tick(); + + if (mEnvironment.getStateManager()->getState()== + MWBase::StateManager::State_Running) + { + MWWorld::Ptr player = mEnvironment.getWorld()->getPlayerPtr(); + if(!guiActive && player.getClass().getCreatureStats(player).isDead()) + mEnvironment.getStateManager()->endGame(); + } + + // update physics + osg::Timer_t beforePhysicsTick = osg::Timer::instance()->tick(); + if (mEnvironment.getStateManager()->getState()!= + MWBase::StateManager::State_NoGame) + { + mEnvironment.getWorld()->updatePhysics(frametime, guiActive); + } + osg::Timer_t afterPhysicsTick = osg::Timer::instance()->tick(); + + // update world + osg::Timer_t beforeWorldTick = osg::Timer::instance()->tick(); + if (mEnvironment.getStateManager()->getState()!= + MWBase::StateManager::State_NoGame) + { + mEnvironment.getWorld()->update(frametime, guiActive); + } + osg::Timer_t afterWorldTick = osg::Timer::instance()->tick(); + + // update GUI + mEnvironment.getWindowManager()->onFrame(frametime); + + unsigned int frameNumber = mViewer->getFrameStamp()->getFrameNumber(); + osg::Stats* stats = mViewer->getViewerStats(); + stats->setAttribute(frameNumber, "script_time_begin", osg::Timer::instance()->delta_s(mStartTick, beforeScriptTick)); + stats->setAttribute(frameNumber, "script_time_taken", osg::Timer::instance()->delta_s(beforeScriptTick, afterScriptTick)); + stats->setAttribute(frameNumber, "script_time_end", osg::Timer::instance()->delta_s(mStartTick, afterScriptTick)); + + stats->setAttribute(frameNumber, "mechanics_time_begin", osg::Timer::instance()->delta_s(mStartTick, beforeMechanicsTick)); + stats->setAttribute(frameNumber, "mechanics_time_taken", osg::Timer::instance()->delta_s(beforeMechanicsTick, afterMechanicsTick)); + stats->setAttribute(frameNumber, "mechanics_time_end", osg::Timer::instance()->delta_s(mStartTick, afterMechanicsTick)); + + stats->setAttribute(frameNumber, "physics_time_begin", osg::Timer::instance()->delta_s(mStartTick, beforePhysicsTick)); + stats->setAttribute(frameNumber, "physics_time_taken", osg::Timer::instance()->delta_s(beforePhysicsTick, afterPhysicsTick)); + stats->setAttribute(frameNumber, "physics_time_end", osg::Timer::instance()->delta_s(mStartTick, afterPhysicsTick)); + + stats->setAttribute(frameNumber, "world_time_begin", osg::Timer::instance()->delta_s(mStartTick, beforeWorldTick)); + stats->setAttribute(frameNumber, "world_time_taken", osg::Timer::instance()->delta_s(beforeWorldTick, afterWorldTick)); + stats->setAttribute(frameNumber, "world_time_end", osg::Timer::instance()->delta_s(mStartTick, afterWorldTick)); + + if (stats->collectStats("resource")) + { + mResourceSystem->reportStats(frameNumber, stats); + + stats->setAttribute(frameNumber, "WorkQueue", mWorkQueue->getNumItems()); + stats->setAttribute(frameNumber, "WorkThread", mWorkQueue->getNumActiveThreads()); + + mEnvironment.getWorld()->getNavigator()->reportStats(frameNumber, *stats); + } + + } + catch (const std::exception& e) + { + Log(Debug::Error) << "Error in frame: " << e.what(); + } + return true; +} + +OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) + : mWindow(nullptr) + , mEncoding(ToUTF8::WINDOWS_1252) + , mEncoder(nullptr) + , mScreenCaptureOperation(nullptr) + , mSkipMenu (false) + , mUseSound (true) + , mCompileAll (false) + , mCompileAllDialogue (false) + , mWarningsMode (1) + , mScriptConsoleMode (false) + , mActivationDistanceOverride(-1) + , mGrab(true) + , mExportFonts(false) + , mRandomSeed(0) + , mScriptContext (0) + , mFSStrict (false) + , mScriptBlacklistUse (true) + , mNewGame (false) + , mCfgMgr(configurationManager) +{ + MWClass::registerClasses(); + + SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); // We use only gamepads + + Uint32 flags = SDL_INIT_VIDEO|SDL_INIT_NOPARACHUTE|SDL_INIT_GAMECONTROLLER|SDL_INIT_JOYSTICK|SDL_INIT_SENSOR; + if(SDL_WasInit(flags) == 0) + { + SDL_SetMainReady(); + if(SDL_Init(flags) != 0) + { + throw std::runtime_error("Could not initialize SDL! " + std::string(SDL_GetError())); + } + } + + mStartTick = osg::Timer::instance()->tick(); +} + +OMW::Engine::~Engine() +{ + mEnvironment.cleanup(); + + delete mScriptContext; + mScriptContext = nullptr; + + mWorkQueue = nullptr; + + mViewer = nullptr; + + mResourceSystem.reset(); + + delete mEncoder; + mEncoder = nullptr; + + if (mWindow) + { + SDL_DestroyWindow(mWindow); + mWindow = nullptr; + } + + SDL_Quit(); +} + +void OMW::Engine::enableFSStrict(bool fsStrict) +{ + mFSStrict = fsStrict; +} + +// Set data dir + +void OMW::Engine::setDataDirs (const Files::PathContainer& dataDirs) +{ + mDataDirs = dataDirs; + mDataDirs.insert(mDataDirs.begin(), (mResDir / "vfs")); + mFileCollections = Files::Collections (mDataDirs, !mFSStrict); +} + +// Add BSA archive +void OMW::Engine::addArchive (const std::string& archive) { + mArchives.push_back(archive); +} + +// Set resource dir +void OMW::Engine::setResourceDir (const boost::filesystem::path& parResDir) +{ + mResDir = parResDir; +} + +// Set start cell name +void OMW::Engine::setCell (const std::string& cellName) +{ + mCellName = cellName; +} + +void OMW::Engine::addContentFile(const std::string& file) +{ + mContentFiles.push_back(file); +} + +void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame) +{ + mSkipMenu = skipMenu; + mNewGame = newGame; +} + +std::string OMW::Engine::loadSettings (Settings::Manager & settings) +{ + // Create the settings manager and load default settings file + const std::string localdefault = (mCfgMgr.getLocalPath() / "settings-default.cfg").string(); + const std::string globaldefault = (mCfgMgr.getGlobalPath() / "settings-default.cfg").string(); + + // prefer local + if (boost::filesystem::exists(localdefault)) + settings.loadDefault(localdefault); + else if (boost::filesystem::exists(globaldefault)) + settings.loadDefault(globaldefault); + else + throw std::runtime_error ("No default settings file found! Make sure the file \"settings-default.cfg\" was properly installed."); + + // load user settings if they exist + const std::string settingspath = (mCfgMgr.getUserConfigPath() / "settings.cfg").string(); + if (boost::filesystem::exists(settingspath)) + settings.loadUser(settingspath); + + return settingspath; +} + +void OMW::Engine::createWindow(Settings::Manager& settings) +{ + int screen = settings.getInt("screen", "Video"); + int width = settings.getInt("resolution x", "Video"); + int height = settings.getInt("resolution y", "Video"); + bool fullscreen = settings.getBool("fullscreen", "Video"); + bool windowBorder = settings.getBool("window border", "Video"); + bool vsync = settings.getBool("vsync", "Video"); + int antialiasing = settings.getInt("antialiasing", "Video"); + + int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), + pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); + + if(fullscreen) + { + pos_x = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); + pos_y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(screen); + } + + Uint32 flags = SDL_WINDOW_OPENGL|SDL_WINDOW_SHOWN|SDL_WINDOW_RESIZABLE; + if(fullscreen) + flags |= SDL_WINDOW_FULLSCREEN; + + if (!windowBorder) + flags |= SDL_WINDOW_BORDERLESS; + + SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, + settings.getBool("minimize on focus loss", "Video") ? "1" : "0"); + + checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8)); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24)); + + if (antialiasing > 0) + { + checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1)); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); + } + + while (!mWindow) + { + mWindow = SDL_CreateWindow("OpenMW", pos_x, pos_y, width, height, flags); + if (!mWindow) + { + // Try with a lower AA + if (antialiasing > 0) + { + Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing/2; + antialiasing /= 2; + Settings::Manager::setInt("antialiasing", "Video", antialiasing); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); + continue; + } + else + { + std::stringstream error; + error << "Failed to create SDL window: " << SDL_GetError(); + throw std::runtime_error(error.str()); + } + } + } + + setWindowIcon(); + + osg::ref_ptr traits = new osg::GraphicsContext::Traits; + SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); + SDL_GetWindowSize(mWindow, &traits->width, &traits->height); + traits->windowName = SDL_GetWindowTitle(mWindow); + traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); + traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); + // We tried to get rid of the hardcoding but failed: https://github.com/OpenMW/openmw/pull/1771 + // Here goes kcat's quote: + // It's ultimately a chicken and egg problem, and the reason why the code is like it was in the first place. + // It needs a context to get the current attributes, but it needs the attributes to set up the context. + // So it just specifies the same values that were given to SDL in the hopes that it's good enough to what the window eventually gets. + traits->red = 8; + traits->green = 8; + traits->blue = 8; + traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel + traits->depth = 24; + traits->stencil = 8; + traits->vsync = vsync; + traits->doubleBuffer = true; + traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); + + osg::ref_ptr graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); + if(!graphicsWindow->valid()) throw std::runtime_error("Failed to create GraphicsContext"); + + osg::ref_ptr camera = mViewer->getCamera(); + camera->setGraphicsContext(graphicsWindow); + camera->setViewport(0, 0, traits->width, traits->height); + +#ifdef USE_OPENXR + initVr(); +#endif + + mViewer->realize(); + + mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, traits->width, traits->height); +} + +void OMW::Engine::setWindowIcon() +{ + boost::filesystem::ifstream windowIconStream; + std::string windowIcon = (mResDir / "mygui" / "openmw.png").string(); + windowIconStream.open(windowIcon, std::ios_base::in | std::ios_base::binary); + if (windowIconStream.fail()) + Log(Debug::Error) << "Error: Failed to open " << windowIcon; + osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension("png"); + if (!reader) + { + Log(Debug::Error) << "Error: Failed to read window icon, no png readerwriter found"; + return; + } + osgDB::ReaderWriter::ReadResult result = reader->readImage(windowIconStream); + if (!result.success()) + Log(Debug::Error) << "Error: Failed to read " << windowIcon << ": " << result.message() << " code " << result.status(); + else + { + osg::ref_ptr image = result.getImage(); + auto surface = SDLUtil::imageToSurface(image, true); + SDL_SetWindowIcon(mWindow, surface.get()); + } +} + +void OMW::Engine::prepareEngine (Settings::Manager & settings) +{ + mEnvironment.setStateManager ( + new MWState::StateManager (mCfgMgr.getUserDataPath() / "saves", mContentFiles.at (0))); + + createWindow(settings); + + osg::ref_ptr rootNode (new osg::Group); + + mViewer->setSceneData(rootNode); + + mVFS.reset(new VFS::Manager(mFSStrict)); + + VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); + + mResourceSystem.reset(new Resource::ResourceSystem(mVFS.get())); + mResourceSystem->getSceneManager()->setUnRefImageDataAfterApply(false); // keep to Off for now to allow better state sharing + mResourceSystem->getSceneManager()->setFilterSettings( + Settings::Manager::getString("texture mag filter", "General"), + Settings::Manager::getString("texture min filter", "General"), + Settings::Manager::getString("texture mipmap", "General"), + Settings::Manager::getInt("anisotropy", "General") + ); + + int numThreads = Settings::Manager::getInt("preload num threads", "Cells"); + if (numThreads <= 0) + throw std::runtime_error("Invalid setting: 'preload num threads' must be >0"); + mWorkQueue = new SceneUtil::WorkQueue(numThreads); + + // Create input and UI first to set up a bootstrapping environment for + // showing a loading screen and keeping the window responsive while doing so + + std::string keybinderUser = (mCfgMgr.getUserConfigPath() / "input_v3.xml").string(); + bool keybinderUserExists = boost::filesystem::exists(keybinderUser); + if(!keybinderUserExists) + { + std::string input2 = (mCfgMgr.getUserConfigPath() / "input_v2.xml").string(); + if(boost::filesystem::exists(input2)) { + boost::filesystem::copy_file(input2, keybinderUser); + keybinderUserExists = boost::filesystem::exists(keybinderUser); + Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; + } + } + else + Log(Debug::Info) << "Loading keybindings file: " << keybinderUser; + + const std::string userdefault = mCfgMgr.getUserConfigPath().string() + "/gamecontrollerdb.txt"; + const std::string localdefault = mCfgMgr.getLocalPath().string() + "/gamecontrollerdb.txt"; + const std::string globaldefault = mCfgMgr.getGlobalPath().string() + "/gamecontrollerdb.txt"; + + std::string userGameControllerdb; + if (boost::filesystem::exists(userdefault)){ + userGameControllerdb = userdefault; + } + else + userGameControllerdb = ""; + + std::string gameControllerdb; + if (boost::filesystem::exists(localdefault)) + gameControllerdb = localdefault; + else if (boost::filesystem::exists(globaldefault)) + gameControllerdb = globaldefault; + else + gameControllerdb = ""; //if it doesn't exist, pass in an empty string +<<<<<<< HEAD + MWInput::InputManager* input = +#ifdef USE_OPENXR + new MWVR::OpenXRInputManager(mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); +#else + new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); +#endif + mEnvironment.setInputManager (input); +======= +>>>>>>> 6b44b7f245e12566c26b6bdd92448aeb6dd90a85 + + std::string myguiResources = (mResDir / "mygui").string(); + osg::ref_ptr guiRoot = new osg::Group; + guiRoot->setName("GUI Root"); + guiRoot->setNodeMask(MWRender::Mask_GUI); + rootNode->addChild(guiRoot); + MWGui::WindowManager* window = new MWGui::WindowManager(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), + mCfgMgr.getLogPath().string() + std::string("/"), myguiResources, + mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, + Version::getOpenmwVersionDescription(mResDir.string()), mCfgMgr.getUserConfigPath().string()); + mEnvironment.setWindowManager (window); + + MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, userGameControllerdb, gameControllerdb, mGrab); + mEnvironment.setInputManager (input); + + // Create sound system + mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mUseSound)); + +#ifdef USE_OPENXR + mXrEnvironment.setGUIManager(new MWVR::VRGUIManager(mViewer)); +#endif + + if (!mSkipMenu) + { + const std::string& logo = Fallback::Map::getString("Movies_Company_Logo"); + if (!logo.empty()) + window->playVideo(logo, true); + } + + // Create the world + mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(), + mFileCollections, mContentFiles, mEncoder, mActivationDistanceOverride, mCellName, + mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string())); + mEnvironment.getWorld()->setupPlayer(); + + window->setStore(mEnvironment.getWorld()->getStore()); + window->initUI(); + + //Load translation data + mTranslationDataStorage.setEncoder(mEncoder); + for (size_t i = 0; i < mContentFiles.size(); i++) + mTranslationDataStorage.loadTranslationData(mFileCollections, mContentFiles[i]); + + Compiler::registerExtensions (mExtensions); + + // Create script system + mScriptContext = new MWScript::CompilerContext (MWScript::CompilerContext::Type_Full); + mScriptContext->setExtensions (&mExtensions); + + mEnvironment.setScriptManager (new MWScript::ScriptManager (mEnvironment.getWorld()->getStore(), *mScriptContext, mWarningsMode, + mScriptBlacklistUse ? mScriptBlacklist : std::vector())); + + // Create game mechanics system + MWMechanics::MechanicsManager* mechanics = new MWMechanics::MechanicsManager; + mEnvironment.setMechanicsManager (mechanics); + + // Create dialog system + mEnvironment.setJournal (new MWDialogue::Journal); + mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mTranslationDataStorage)); + + // scripts + if (mCompileAll) + { + std::pair result = mEnvironment.getScriptManager()->compileAll(); + if (result.first) + Log(Debug::Info) + << "compiled " << result.second << " of " << result.first << " scripts (" + << 100*static_cast (result.second)/result.first + << "%)"; + } + if (mCompileAllDialogue) + { + std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions, mWarningsMode); + if (result.first) + Log(Debug::Info) + << "compiled " << result.second << " of " << result.first << " dialogue script/actor combinations a(" + << 100*static_cast (result.second)/result.first + << "%)"; + } +} + +class WriteScreenshotToFileOperation : public osgViewer::ScreenCaptureHandler::CaptureOperation +{ +public: + WriteScreenshotToFileOperation(const std::string& screenshotPath, const std::string& screenshotFormat) + : mScreenshotPath(screenshotPath) + , mScreenshotFormat(screenshotFormat) + { + } + + virtual void operator()(const osg::Image& image, const unsigned int context_id) + { + // Count screenshots. + int shotCount = 0; + + // Find the first unused filename with a do-while + std::ostringstream stream; + do + { + // Reset the stream + stream.str(""); + stream.clear(); + + stream << mScreenshotPath << "/screenshot" << std::setw(3) << std::setfill('0') << shotCount++ << "." << mScreenshotFormat; + + } while (boost::filesystem::exists(stream.str())); + + boost::filesystem::ofstream outStream; + outStream.open(boost::filesystem::path(stream.str()), std::ios::binary); + + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension(mScreenshotFormat); + if (!readerwriter) + { + Log(Debug::Error) << "Error: Can't write screenshot, no '" << mScreenshotFormat << "' readerwriter found"; + return; + } + + osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(image, outStream); + if (!result.success()) + { + Log(Debug::Error) << "Error: Can't write screenshot: " << result.message() << " code " << result.status(); + } + } + +private: + std::string mScreenshotPath; + std::string mScreenshotFormat; +}; + +// Initialise and enter main loop. +void OMW::Engine::go() +{ + assert (!mContentFiles.empty()); + + Log(Debug::Info) << "OSG version: " << osgGetVersion(); + SDL_version sdlVersion; + SDL_GetVersion(&sdlVersion); + Log(Debug::Info) << "SDL version: " << (int)sdlVersion.major << "." << (int)sdlVersion.minor << "." << (int)sdlVersion.patch; + + Misc::Rng::init(mRandomSeed); + + // Load settings + Settings::Manager settings; + std::string settingspath; + settingspath = loadSettings (settings); + + // Create encoder + mEncoder = new ToUTF8::Utf8Encoder(mEncoding); + + // Setup viewer + mViewer = new osgViewer::Viewer; + mViewer->setReleaseContextAtEndOfFrameHint(false); + +#if OSG_VERSION_GREATER_OR_EQUAL(3,5,5) + // Do not try to outsmart the OS thread scheduler (see bug #4785). + mViewer->setUseConfigureAffinity(false); +#endif + + mScreenCaptureOperation = new WriteScreenshotToFileOperation( + mCfgMgr.getScreenshotPath().string(), + Settings::Manager::getString("screenshot format", "General")); + + mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); + + mViewer->addEventHandler(mScreenCaptureHandler); + + mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); + + prepareEngine (settings); + + // Setup profiler + osg::ref_ptr statshandler = new Resource::Profiler; + + statshandler->addUserStatsLine("Script", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "script_time_taken", 1000.0, true, false, "script_time_begin", "script_time_end", 10000); + statshandler->addUserStatsLine("Mech", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "mechanics_time_taken", 1000.0, true, false, "mechanics_time_begin", "mechanics_time_end", 10000); + statshandler->addUserStatsLine("Phys", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "physics_time_taken", 1000.0, true, false, "physics_time_begin", "physics_time_end", 10000); + statshandler->addUserStatsLine("World", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "world_time_taken", 1000.0, true, false, "world_time_begin", "world_time_end", 10000); + + mViewer->addEventHandler(statshandler); + + osg::ref_ptr resourceshandler = new Resource::StatsHandler; + mViewer->addEventHandler(resourceshandler); + +#ifdef USE_OPENXR + auto* root = mViewer->getSceneData(); + auto* xrViewer = MWVR::Environment::get().getViewer(); + xrViewer->addChild(root); + mViewer->setSceneData(xrViewer); + mXrEnvironment.setGUIManager(new MWVR::VRGUIManager(mViewer)); +#endif + + // Start the game + if (!mSaveGameFile.empty()) + { + mEnvironment.getStateManager()->loadGame(mSaveGameFile); + } + else if (!mSkipMenu) + { + // start in main menu + mEnvironment.getWindowManager()->pushGuiMode (MWGui::GM_MainMenu); + mEnvironment.getSoundManager()->playTitleMusic(); + const std::string& logo = Fallback::Map::getString("Movies_Morrowind_Logo"); + if (!logo.empty()) + mEnvironment.getWindowManager()->playVideo(logo, true); + } + else + { + mEnvironment.getStateManager()->newGame (!mNewGame); + } + + if (!mStartupScript.empty() && mEnvironment.getStateManager()->getState() == MWState::StateManager::State_Running) + { + mEnvironment.getWindowManager()->executeInConsole(mStartupScript); + } + + + + // Start the main rendering loop + osg::Timer frameTimer; + double simulationTime = 0.0; + while (!mViewer->done() && !mEnvironment.getStateManager()->hasQuitRequest()) + { + double dt = frameTimer.time_s(); + frameTimer.setStartTick(); + dt = std::min(dt, 0.2); + + mViewer->advance(simulationTime); + + if (!frame(dt)) + { + OpenThreads::Thread::microSleep(5000); + continue; + } + else + { + + mViewer->eventTraversal(); + + mEnvironment.getWorld()->updateWindowManager(); + +#ifdef USE_OPENXR + xrViewer->traversals(); +#else + mViewer->updateTraversal(); + + mViewer->renderingTraversals(); +#endif + + bool guiActive = mEnvironment.getWindowManager()->isGuiMode(); + if (!guiActive) + simulationTime += dt; + } + + mEnvironment.limitFrameRate(frameTimer.time_s()); + } + + // Save user settings + settings.saveUser(settingspath); + + Log(Debug::Info) << "Quitting peacefully."; +} + +void OMW::Engine::setCompileAll (bool all) +{ + mCompileAll = all; +} + +void OMW::Engine::setCompileAllDialogue (bool all) +{ + mCompileAllDialogue = all; +} + +void OMW::Engine::setSoundUsage(bool soundUsage) +{ + mUseSound = soundUsage; +} + +void OMW::Engine::setEncoding(const ToUTF8::FromType& encoding) +{ + mEncoding = encoding; +} + +void OMW::Engine::setScriptConsoleMode (bool enabled) +{ + mScriptConsoleMode = enabled; +} + +void OMW::Engine::setStartupScript (const std::string& path) +{ + mStartupScript = path; +} + +void OMW::Engine::setActivationDistanceOverride (int distance) +{ + mActivationDistanceOverride = distance; +} + +void OMW::Engine::setWarningsMode (int mode) +{ + mWarningsMode = mode; +} + +void OMW::Engine::setScriptBlacklist (const std::vector& list) +{ + mScriptBlacklist = list; +} + +void OMW::Engine::setScriptBlacklistUse (bool use) +{ + mScriptBlacklistUse = use; +} + +void OMW::Engine::enableFontExport(bool exportFonts) +{ + mExportFonts = exportFonts; +} + +void OMW::Engine::setSaveGameFile(const std::string &savegame) +{ + mSaveGameFile = savegame; +} + +void OMW::Engine::setRandomSeed(unsigned int seed) +{ + mRandomSeed = seed; +} diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 1aef62df5..416a5eec0 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -13,6 +13,10 @@ #include "mwworld/ptr.hpp" +#ifdef USE_OPENXR +#include "mwvr/vrenvironment.hpp" +#endif + namespace Resource { class ResourceSystem; @@ -33,6 +37,12 @@ namespace Compiler class Context; } +namespace Misc +{ + class StereoView; + class CallbackManager; +} + namespace Files { struct ConfigurationManager; @@ -66,6 +76,13 @@ namespace OMW std::string mCellName; std::vector mContentFiles; std::vector mGroundcoverFiles; + + bool mStereoEnabled; + bool mStereoOverride; + std::unique_ptr mStereoView; + + std::unique_ptr mCallbackManager; + bool mSkipMenu; bool mUseSound; bool mCompileAll; @@ -185,6 +202,12 @@ namespace OMW private: Files::ConfigurationManager& mCfgMgr; + +#ifdef USE_OPENXR + MWVR::Environment mXrEnvironment; + + void initVr(); +#endif }; } diff --git a/apps/openmw/mwbase/environment.cpp b/apps/openmw/mwbase/environment.cpp index b7235edd4..971343474 100644 --- a/apps/openmw/mwbase/environment.cpp +++ b/apps/openmw/mwbase/environment.cpp @@ -19,7 +19,7 @@ MWBase::Environment *MWBase::Environment::sThis = nullptr; MWBase::Environment::Environment() : mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr), mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (nullptr), - mStateManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f) + mStateManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f), mVrMode(false) { assert (!sThis); sThis = this; @@ -96,6 +96,16 @@ float MWBase::Environment::getFrameRateLimit() const return mFrameRateLimit; } +void MWBase::Environment::setVrMode(bool vrMode) +{ + mVrMode = vrMode; +} + +bool MWBase::Environment::getVrMode(void) const +{ + return mVrMode; +} + MWBase::World *MWBase::Environment::getWorld() const { assert (mWorld); diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index 3b57e4e7c..284cbdd52 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -45,6 +45,7 @@ namespace MWBase Resource::ResourceSystem *mResourceSystem; float mFrameDuration; float mFrameRateLimit; + bool mVrMode; Environment (const Environment&); ///< not implemented @@ -84,6 +85,9 @@ namespace MWBase void setFrameRateLimit(float frameRateLimit); float getFrameRateLimit() const; + void setVrMode(bool vrMode); + bool getVrMode(void) const; + World *getWorld() const; SoundManager *getSoundManager() const; diff --git a/apps/openmw/mwbase/inputmanager.hpp b/apps/openmw/mwbase/inputmanager.hpp index 951b5053a..857aee8ec 100644 --- a/apps/openmw/mwbase/inputmanager.hpp +++ b/apps/openmw/mwbase/inputmanager.hpp @@ -63,6 +63,8 @@ namespace MWBase virtual void enableDetectingBindingMode (int action, bool keyboard) = 0; virtual void resetToDefaultKeyBindings() = 0; virtual void resetToDefaultControllerBindings() = 0; + virtual void applyHapticsLeftHand(float intensity) = 0; + virtual void applyHapticsRightHand(float intensity) = 0; /// Returns if the last used input device was a joystick or a keyboard /// @return true if joystick, false otherwise diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 15d977f48..c6aff728f 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -54,7 +54,7 @@ namespace MWWorld namespace MWGui { class Layout; - + class DragAndDrop; class Console; class SpellWindow; class TradeWindow; @@ -106,6 +106,7 @@ namespace MWBase /// @note This method will block until the video finishes playing /// (and will continually update the window while doing so) virtual void playVideo(const std::string& name, bool allowSkipping) = 0; + virtual bool isPlayingVideo(void) const = 0; virtual void setNewGame(bool newgame) = 0; @@ -289,6 +290,7 @@ namespace MWBase virtual void showCrosshair(bool show) = 0; virtual bool getSubtitlesEnabled() = 0; virtual bool toggleHud() = 0; + virtual MWGui::DragAndDrop& getDragAndDrop(void) = 0; virtual void disallowMouse() = 0; virtual void allowMouse() = 0; @@ -444,6 +446,8 @@ namespace MWBase virtual void watchActor(const MWWorld::Ptr& ptr) = 0; virtual MWWorld::Ptr getWatchedActor() const = 0; + + virtual void viewerTraversals(bool updateWindowManager) = 0; }; } diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 01c1c55cd..89e32f79b 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -33,7 +34,9 @@ namespace osg class Matrixf; class Quat; class Image; + class Node; class Stats; + class Transform; } namespace Loading @@ -72,6 +75,8 @@ namespace MWPhysics namespace MWRender { class Animation; + class RenderingManager; + struct RayResult; } namespace MWMechanics @@ -96,6 +101,11 @@ namespace MWWorld typedef std::vector > PtrMovementList; } +namespace MWVR +{ + class UserPointer; +} + namespace MWBase { /// \brief Interface for the World (implemented in MWWorld) @@ -156,6 +166,8 @@ namespace MWBase virtual MWWorld::Ptr getPlayerPtr() = 0; virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; + virtual MWRender::RenderingManager& getRenderingManager() = 0; + virtual const MWWorld::ESMStore& getStore() const = 0; /* @@ -385,6 +397,8 @@ namespace MWBase virtual float getMaxActivationDistance() = 0; + virtual float getActivationDistancePlusTelekinesis() = 0; + /// Returns a pointer to the object the provided object would hit (if within the /// specified distance), and the point where the hit occurs. This will attempt to /// use the "Head" node, or alternatively the "Bip01 Head" node as a basis. @@ -563,6 +577,12 @@ namespace MWBase /// @param cursor Y (relative 0-1) /// @param number of objects to place + virtual MWWorld::Ptr placeObject (const MWWorld::ConstPtr& object, const MWRender::RayResult& ray, int amount) = 0; + ///< copy and place an object into the gameworld based on the given intersection + /// @param object + /// @param world position to place object + /// @param number of objects to place + virtual MWWorld::Ptr dropObjectOnGround (const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object, int amount) = 0; ///< copy and place an object into the gameworld at the given actor's position /// @param actor giving the dropped object position @@ -857,6 +877,18 @@ namespace MWBase virtual bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; + /// @result pointer to the object and/or node the given node is currently pointing at + /// @Return distance to the target object, or -1 if no object was targeted / in range + virtual float getTargetObject(MWRender::RayResult& result, const osg::Vec3f& origin, const osg::Quat& orientation, float maxDistance, bool ignorePlayer) = 0; + +#ifdef USE_OPENXR + virtual MWVR::UserPointer& getUserPointer() = 0; + virtual MWWorld::Ptr getPointerTarget() = 0; +#endif + + /// @Return ESM::Weapon::Type enum describing the type of weapon currently drawn by the player. + virtual int getActiveWeaponType(void) = 0; + virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 24883919b..20aa9c08a 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -31,6 +31,7 @@ #include "../mwmechanics/difficultyscaling.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" @@ -237,7 +238,7 @@ namespace MWClass } - void Creature::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const + bool Creature::hit(const MWWorld::Ptr& ptr, float attackStrength, int type, bool) const { /* Start of tes3mp addition @@ -246,7 +247,7 @@ namespace MWClass */ if (mwmp::PlayerList::isDedicatedPlayer(ptr) || mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) { - return; + return false; } /* End of tes3mp addition @@ -258,7 +259,7 @@ namespace MWClass MWMechanics::CreatureStats &stats = getCreatureStats(ptr); if (stats.getDrawState() != MWMechanics::DrawState_Weapon) - return; + return false; // Get the weapon used (if hand-to-hand, weapon = inv.end()) MWWorld::Ptr weapon; @@ -282,7 +283,7 @@ namespace MWClass std::pair result = MWBase::Environment::get().getWorld()->getHitContact(ptr, dist, targetActors); if (result.first.isEmpty()) - return; // Didn't hit anything + return false; // Didn't hit anything MWWorld::Ptr victim = result.first; @@ -299,6 +300,7 @@ namespace MWClass objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(victim, ptr); objectList->sendObjectHit(); + return false; } /* End of tes3mp change (major) @@ -355,7 +357,7 @@ namespace MWClass victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); - return; + return true; } int min,max; @@ -423,10 +425,11 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, attackStrength); + return true; } - void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const + void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, float hitStrength) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); @@ -441,6 +444,8 @@ namespace MWClass if (isMobile(ptr) && !attacker.isEmpty()) setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); + bool attackerIsPlayer = attacker == MWMechanics::getPlayer(); + // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) { @@ -480,7 +485,7 @@ namespace MWClass if (!object.isEmpty()) stats.setLastHitAttemptObject(object.getCellRef().getRefId()); - if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) + if (setOnPcHitMe && !attacker.isEmpty() && attackerIsPlayer) { const std::string &script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -491,7 +496,7 @@ namespace MWClass if (!successful) { // Missed - if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) + if (!attacker.isEmpty() && attackerIsPlayer) MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); return; } @@ -607,6 +612,16 @@ namespace MWClass /* End of tes3mp addition */ + + if(successful) + { + auto* inputManager = MWBase::Environment::get().getInputManager(); + if (attackerIsPlayer && hitStrength > 0.f) + { + float hapticIntensity = std::max(0.25f, std::min(1.f, hitStrength)); + inputManager->applyHapticsRightHand(hapticIntensity); + } + } } std::shared_ptr Creature::activate (const MWWorld::Ptr& ptr, diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index 8ecb28c04..cf01addb3 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -55,9 +55,9 @@ namespace MWClass MWMechanics::CreatureStats& getCreatureStats (const MWWorld::Ptr& ptr) const override; ///< Return creature stats - void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const override; + bool hit(const MWWorld::Ptr& ptr, float attackStrength, int type, bool simulated) const override; - void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const override; + void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, float hitStrength) const override; std::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 1c5dee64f..0d302d411 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -29,6 +29,7 @@ */ #include "../mwbase/environment.hpp" +#include "../mwbase/inputmanager.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -548,7 +549,7 @@ namespace MWClass } - void Npc::hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const + bool Npc::hit(const MWWorld::Ptr& ptr, float attackStrength, int type, bool simulated) const { /* Start of tes3mp addition @@ -557,7 +558,7 @@ namespace MWClass */ if (mwmp::PlayerList::isDedicatedPlayer(ptr) || mwmp::Main::get().getCellController()->isDedicatedActor(ptr)) { - return; + return false; } /* End of tes3mp addition @@ -574,7 +575,11 @@ namespace MWClass if(!weapon.isEmpty() && weapon.getTypeName() != typeid(ESM::Weapon).name()) weapon = MWWorld::Ptr(); - MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); + if (getCreatureStats(ptr).getDrawState() != MWMechanics::DrawState_Weapon) + return false; + + if (!simulated) + MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); const float fCombatDistance = store.find("fCombatDistance")->mValue.getFloat(); float dist = fCombatDistance * (!weapon.isEmpty() ? @@ -591,9 +596,10 @@ namespace MWClass MWWorld::Ptr victim = result.first; osg::Vec3f hitPosition (result.second); if(victim.isEmpty()) // Didn't hit anything - return; + return false; const MWWorld::Class &othercls = victim.getClass(); + /* Start of tes3mp change (major) @@ -607,13 +613,18 @@ namespace MWClass objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; objectList->addObjectHit(victim, ptr); objectList->sendObjectHit(); + return false; } /* End of tes3mp change (major) */ + MWMechanics::CreatureStats &otherstats = othercls.getCreatureStats(victim); if(otherstats.isDead()) // Can't hit dead actors - return; + return false; + + if (simulated) + return true; if(ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); @@ -671,7 +682,7 @@ namespace MWClass othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); - return; + return true; } bool healthdmg; @@ -744,14 +755,16 @@ namespace MWClass MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, attackStrength); + return true; } - void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const + void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, float hitStrength) const { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); + float rawDamage = damage; // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; @@ -762,6 +775,8 @@ namespace MWClass stats.setAttacked(true); setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } + bool attackerIsPlayer = attacker == MWMechanics::getPlayer(); + bool victimIsPlayer = ptr == MWMechanics::getPlayer(); // Attacker and target store each other as hitattemptactor if they have no one stored yet if (!attacker.isEmpty() && attacker.getClass().isActor()) @@ -802,7 +817,7 @@ namespace MWClass if (!object.isEmpty()) stats.setLastHitAttemptObject(object.getCellRef().getRefId()); - if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) + if (setOnPcHitMe && !attacker.isEmpty() && attackerIsPlayer) { const std::string &script = getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -813,7 +828,7 @@ namespace MWClass if (!successful) { // Missed - if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) + if (!attacker.isEmpty() && attackerIsPlayer) sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); return; } @@ -828,7 +843,7 @@ namespace MWClass if (damage < 0.001f) damage = 0; - bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + bool godmode = victimIsPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); if (godmode) damage = 0; @@ -1046,6 +1061,23 @@ namespace MWClass /* End of tes3mp addition */ + + // Apply haptics + if (successful) + { + auto* inputManager = MWBase::Environment::get().getInputManager(); + if (victimIsPlayer) + { + float maxHealth = getCreatureStats(ptr).getHealth().getModified(); + float hapticIntensity = std::max(0.25f, std::min(1.f, rawDamage / ( maxHealth / 4.f))); + inputManager->applyHapticsLeftHand(hapticIntensity); + } + else if (attackerIsPlayer && hitStrength > 0.f) + { + float hapticIntensity = std::max(0.25f, std::min(1.f, hitStrength)); + inputManager->applyHapticsRightHand(hapticIntensity); + } + } } std::shared_ptr Npc::activate (const MWWorld::Ptr& ptr, diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 08aa6c058..ca7e6f264 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -80,9 +80,9 @@ namespace MWClass End of tes3mp addition */ - void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const override; + bool hit(const MWWorld::Ptr& ptr, float attackStrength, int type, bool simulated) const override; - void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const override; + void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, float hitStrength) const override; void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const override; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). diff --git a/apps/openmw/mwgui/alchemywindow.cpp b/apps/openmw/mwgui/alchemywindow.cpp index e174a9f1b..2420799ab 100644 --- a/apps/openmw/mwgui/alchemywindow.cpp +++ b/apps/openmw/mwgui/alchemywindow.cpp @@ -39,6 +39,10 @@ #include "itemwidget.hpp" #include "widgets.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrlistbox.hpp" +#endif + namespace MWGui { AlchemyWindow::AlchemyWindow() @@ -46,6 +50,9 @@ namespace MWGui , mCurrentFilter(FilterType::ByName) , mModel(nullptr) , mSortModel(nullptr) + , mFilterCombo(nullptr) + , mFilterEdit(nullptr) + , mFilterButton(nullptr) , mAlchemy(new MWMechanics::Alchemy()) , mApparatus (4) , mIngredients (4) @@ -66,8 +73,31 @@ namespace MWGui getWidget(mDecreaseButton, "DecreaseButton"); getWidget(mNameEdit, "NameEdit"); getWidget(mItemView, "ItemView"); - getWidget(mFilterValue, "FilterValue"); + getWidget(mFilterCombo, "FilterValue"); getWidget(mFilterType, "FilterType"); + getWidget(mFilterEdit, "FilterEdit"); + getWidget(mFilterButton, "FilterButton"); + + if (MWBase::Environment::get().getVrMode()) + { +#ifdef USE_OPENXR + mFilterListBox = new MWVR::VrListBox(); +#endif + mFilterCombo->setVisible(false); + mFilterCombo->setUserString("Hidden", "true"); + } + else + { + mFilterButton->setVisible(false); + mFilterButton->setUserString("Hidden", "true"); + mFilterEdit->setVisible(false); + mFilterEdit->setUserString("Hidden", "true"); + } + + mFilterButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onFilterButtonClicked); + mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); + mFilterCombo->eventComboChangePosition += MyGUI::newDelegate(this, &AlchemyWindow::onFilterChanged); + mFilterCombo->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); mBrewCountEdit->eventValueChanged += MyGUI::newDelegate(this, &AlchemyWindow::onCountValueChanged); mBrewCountEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); @@ -90,8 +120,7 @@ namespace MWGui mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onCancelButtonClicked); mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); - mFilterValue->eventComboChangePosition += MyGUI::newDelegate(this, &AlchemyWindow::onFilterChanged); - mFilterValue->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); + mFilterType->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::switchFilterType); center(); @@ -210,7 +239,8 @@ namespace MWGui else mCurrentFilter = FilterType::ByEffect; updateFilters(); - mFilterValue->clearIndexSelected(); + mFilterCombo->clearIndexSelected(); + mFilterEdit->setCaption(""); updateFilters(); } @@ -233,39 +263,44 @@ namespace MWGui } mSortModel->setNameFilter({}); mSortModel->setEffectFilter({}); - mFilterValue->clearIndexSelected(); + mFilterCombo->clearIndexSelected(); + mFilterEdit->setCaption(""); updateFilters(); mItemView->update(); } void AlchemyWindow::updateFilters() { - std::set itemNames, itemEffects; + mItemEffects.clear(); + mItemNames.clear(); for (size_t i = 0; i < mModel->getItemCount(); ++i) { MWWorld::Ptr item = mModel->getItem(i).mBase; if (item.getTypeName() != typeid(ESM::Ingredient).name()) continue; - itemNames.insert(item.getClass().getName(item)); + mItemNames.insert(item.getClass().getName(item)); MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); auto const alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); auto const effects = MWMechanics::Alchemy::effectsDescription(item, alchemySkill); - itemEffects.insert(effects.begin(), effects.end()); + mItemEffects.insert(effects.begin(), effects.end()); } - mFilterValue->removeAllItems(); - auto const addItems = [&](auto const& container) + mFilterCombo->removeAllItems(); + for (auto const& item : items()) { - for (auto const& item : container) - mFilterValue->addItem(item); - }; + mFilterCombo->addItem(item); + } + } + + const std::set& AlchemyWindow::items() + { switch (mCurrentFilter) { - case FilterType::ByName: addItems(itemNames); break; - case FilterType::ByEffect: addItems(itemEffects); break; + case FilterType::ByName: return mItemNames; break; + case FilterType::ByEffect: return mItemEffects; break; } } @@ -292,6 +327,20 @@ namespace MWGui applyFilter(_sender->getCaption()); } + void AlchemyWindow::onFilterButtonClicked(MyGUI::Widget* _sender) + { +#ifdef USE_OPENXR + mFilterListBox->open(mFilterCombo, [this](std::size_t index) { + if (index != MyGUI::ITEM_NONE) + { + auto filter = mFilterCombo->getItemNameAt(index); + mFilterEdit->setCaption(filter); + applyFilter(filter); + } + }); +#endif + } + void AlchemyWindow::onOpen() { mAlchemy->clear(); diff --git a/apps/openmw/mwgui/alchemywindow.hpp b/apps/openmw/mwgui/alchemywindow.hpp index 33bd1f974..c9d7ad518 100644 --- a/apps/openmw/mwgui/alchemywindow.hpp +++ b/apps/openmw/mwgui/alchemywindow.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -17,6 +18,11 @@ namespace MWMechanics class Alchemy; } +namespace MWVR +{ + class VrListBox; +} + namespace MWGui { class ItemView; @@ -54,10 +60,16 @@ namespace MWGui MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; Gui::AutoSizedButton* mFilterType; - MyGUI::ComboBox* mFilterValue; + MyGUI::ComboBox* mFilterCombo; + MyGUI::EditBox* mFilterEdit; + MyGUI::Button* mFilterButton; + MWVR::VrListBox* mFilterListBox; MyGUI::EditBox* mNameEdit; Gui::NumericEditBox* mBrewCountEdit; + std::set mItemNames; + std::set mItemEffects; + void onCancelButtonClicked(MyGUI::Widget* _sender); void onCreateButtonClicked(MyGUI::Widget* _sender); void onIngredientSelected(MyGUI::Widget* _sender); @@ -72,8 +84,10 @@ namespace MWGui void initFilter(); void onFilterChanged(MyGUI::ComboBox* _sender, size_t _index); void onFilterEdited(MyGUI::EditBox* _sender); + void onFilterButtonClicked(MyGUI::Widget* _sender); void switchFilterType(MyGUI::Widget* _sender); void updateFilters(); + const std::set& items(); void addRepeatController(MyGUI::Widget* widget); diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index af0742d23..4dfc51d45 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -48,7 +48,11 @@ namespace MWGui { ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop) +#ifdef USE_OPENXR + : WindowBase("openmw_container_window_vr.layout") +#else : WindowBase("openmw_container_window.layout") +#endif , mDragAndDrop(dragAndDrop) , mSortModel(nullptr) , mModel(nullptr) diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index ab8afee9c..54901eee5 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -279,7 +279,11 @@ namespace MWGui // -------------------------------------------------------------------------------------------------- DialogueWindow::DialogueWindow() +#ifdef USE_OPENXR + : WindowBase("openmw_dialogue_window_vr.layout") +#else : WindowBase("openmw_dialogue_window.layout") +#endif , mIsCompanion(false) , mGoodbye(false) , mPersuasionDialog(new ResponseCallback(this)) diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 1a397ca90..eb2d3da3a 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -106,7 +106,11 @@ namespace MWGui HUD::HUD(CustomMarkerCollection &customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender) +#ifdef USE_OPENXR + : WindowBase("openmw_hud_vr.layout") +#else : WindowBase("openmw_hud.layout") +#endif , LocalMapBase(customMarkers, localMapRender, Settings::Manager::getBool("local map hud fog of war", "Map")) , mHealth(nullptr) , mMagicka(nullptr) @@ -139,7 +143,13 @@ namespace MWGui , mIsDrowning(false) , mDrowningFlashTheta(0.f) { +#ifdef USE_OPENXR + mMainWidgetBaseSize = mMainWidget->getSize(); + mMainWidget->setSize(mMainWidgetBaseSize); +#else mMainWidget->setSize(MyGUI::RenderManager::getInstance().getViewSize()); + mMainWidgetBaseSize = mMainWidget->getSize(); +#endif // Energy bars getWidget(mHealthFrame, "HealthFrame"); @@ -327,6 +337,7 @@ namespace MWGui } } + // XR-TODO: Implement equivalent void HUD::onWorldMouseOver(MyGUI::Widget* _sender, int x, int y) { if (mDragAndDrop->mIsOnDragAndDrop) @@ -641,8 +652,8 @@ namespace MWGui mSpellBox->setPosition(mSpellBoxBaseLeft - spellDx, mSpellBox->getTop()); mSneakBox->setPosition(mSneakBoxBaseLeft - sneakDx, mSneakBox->getTop()); +#ifndef USE_OPENXR const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); - // effect box can have variable width -> variable left coordinate int effectsDx = 0; if (!mMinimapBox->getVisible ()) @@ -653,6 +664,11 @@ namespace MWGui mCellNameBox->setVisible(false); mEffectBox->setPosition((viewSize.width - mEffectBoxBaseRight) - mEffectBox->getWidth() + effectsDx, mEffectBox->getTop()); +#else + // in VR mode, the effect box grows to the right and does not need repositioning + int width = std::max(mMainWidgetBaseSize.width, mEffectBox->getSize().width); + mMainWidget->setSize(width, mMainWidget->getHeight()); +#endif } void HUD::updateEnemyHealthBar() diff --git a/apps/openmw/mwgui/hud.hpp b/apps/openmw/mwgui/hud.hpp index 8a89320d8..0761897ec 100644 --- a/apps/openmw/mwgui/hud.hpp +++ b/apps/openmw/mwgui/hud.hpp @@ -79,6 +79,8 @@ namespace MWGui int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft; // bottom right elements int mMinimapBoxBaseRight, mEffectBoxBaseRight; + // initial size + MyGUI::IntSize mMainWidgetBaseSize; DragAndDrop* mDragAndDrop; diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 2102c163b..45106932e 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -71,7 +71,11 @@ namespace MWGui { InventoryWindow::InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem) +#ifdef USE_OPENXR + : WindowPinnableBase("openmw_inventory_window_vr.layout") +#else : WindowPinnableBase("openmw_inventory_window.layout") +#endif , mDragAndDrop(dragAndDrop) , mSelectedItem(-1) , mSortModel(nullptr) diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index 214245767..e3cebc849 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -118,7 +118,7 @@ namespace MWGui void sellItem(MyGUI::Widget* sender, int count); void dragItem(MyGUI::Widget* sender, int count); - void onWindowResize(MyGUI::Window* _sender); + void onWindowResize(MyGUI::Window* _sender) override; void onFilterChanged(MyGUI::Widget* _sender); void onNameFilterChanged(MyGUI::EditBox* _sender); void onAvatarClicked(MyGUI::Widget* _sender); diff --git a/apps/openmw/mwgui/keyboardnavigation.cpp b/apps/openmw/mwgui/keyboardnavigation.cpp index f7d54adf6..13493f29f 100644 --- a/apps/openmw/mwgui/keyboardnavigation.cpp +++ b/apps/openmw/mwgui/keyboardnavigation.cpp @@ -22,6 +22,11 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vrgui.hpp" +#endif + namespace MWGui { @@ -102,6 +107,11 @@ void KeyboardNavigation::_unlinkWidget(MyGUI::Widget *widget) w.second = nullptr; if (widget == mCurrentFocus) mCurrentFocus = nullptr; + +#ifdef USE_OPENXR + if (MWBase::Environment::get().getVrMode()) + MWVR::Environment::get().getGUIManager()->notifyWidgetUnlinked(widget); +#endif } #if MYGUI_VERSION < MYGUI_DEFINE_VERSION(3,2,3) diff --git a/apps/openmw/mwgui/layout.cpp b/apps/openmw/mwgui/layout.cpp index 9b9b9537f..21ef66e68 100644 --- a/apps/openmw/mwgui/layout.cpp +++ b/apps/openmw/mwgui/layout.cpp @@ -5,6 +5,13 @@ #include #include #include +#include +#include + +#ifdef USE_OPENXR +#include "../mwvr/vrgui.hpp" +#include "../mwvr/vrenvironment.hpp" +#endif namespace MWGui { @@ -45,9 +52,21 @@ namespace MWGui mMainWidget->setCoord(x,y,w,h); } + void Layout::setCoordf(float x, float y, float w, float h) + { + mMainWidget->setRealCoord(x, y, w, h); + } + void Layout::setVisible(bool b) { mMainWidget->setVisible(b); +#ifdef USE_OPENXR + auto* vrGUIManager = MWVR::Environment::get().getGUIManager(); + if (!vrGUIManager) + // May end up here before before rendering has been fully set up + return; + vrGUIManager->setVisible(this, b); +#endif } void Layout::setText(const std::string &name, const std::string &caption) diff --git a/apps/openmw/mwgui/layout.hpp b/apps/openmw/mwgui/layout.hpp index ea51bf541..910015c1e 100644 --- a/apps/openmw/mwgui/layout.hpp +++ b/apps/openmw/mwgui/layout.hpp @@ -55,6 +55,7 @@ namespace MWGui public: void setCoord(int x, int y, int w, int h); + void setCoordf(float x, float y, float w, float h); virtual void setVisible(bool b); diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 61fcacca4..9fab582b5 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -331,12 +332,7 @@ namespace MWGui mCopyFramebufferToTextureCallback = new CopyFramebufferToTextureCallback(mTexture); } -#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 10) - mViewer->getCamera()->removeInitialDrawCallback(mCopyFramebufferToTextureCallback); - mViewer->getCamera()->addInitialDrawCallback(mCopyFramebufferToTextureCallback); -#else - mViewer->getCamera()->setInitialDrawCallback(mCopyFramebufferToTextureCallback); -#endif + Misc::CallbackManager::instance().addCallbackOneshot(Misc::CallbackManager::DrawStage::Initial, mCopyFramebufferToTextureCallback); mCopyFramebufferToTextureCallback->reset(); mBackgroundImage->setBackgroundImage(""); @@ -375,9 +371,7 @@ namespace MWGui // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); + MWBase::Environment::get().getWindowManager()->viewerTraversals(false); mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); mLastRenderTime = mTimer.time_m(); diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index cc62f4ebb..ba1e00a9b 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -700,7 +700,11 @@ namespace MWGui // ------------------------------------------------------------------------------------------ MapWindow::MapWindow(CustomMarkerCollection &customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue) +#ifdef USE_OPENXR + : WindowPinnableBase("openmw_map_window_vr.layout") +#else : WindowPinnableBase("openmw_map_window.layout") +#endif , LocalMapBase(customMarkers, localMapRender) , NoDrop(drag, mMainWidget) , mGlobalMap(nullptr) diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 6efe1592f..ce6702d41 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -138,6 +138,8 @@ namespace MWGui messageBox->update(height); height += messageBox->getHeight(); } + + box->setVisible(true); } void MessageBoxManager::removeStaticMessageBox () @@ -228,6 +230,11 @@ namespace MWGui mMessageWidget->setCaptionWithReplacing(mMessage); } + MessageBox::~MessageBox() + { + setVisible(false); + } + void MessageBox::update (int height) { MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index 6255c314b..4bf7e9117 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -68,6 +68,7 @@ namespace MWGui { public: MessageBox (MessageBoxManager& parMessageBoxManager, const std::string& message); + ~MessageBox(); void setMessage (const std::string& message); int getHeight (); void update (int height); diff --git a/apps/openmw/mwgui/mode.hpp b/apps/openmw/mwgui/mode.hpp index 62d739657..53a71eaaf 100644 --- a/apps/openmw/mwgui/mode.hpp +++ b/apps/openmw/mwgui/mode.hpp @@ -46,7 +46,8 @@ namespace MWGui GM_LoadingWallpaper, GM_Jail, - GM_QuickKeysMenu + GM_QuickKeysMenu, + GM_VrMetaMenu }; // Windows shown in inventory mode diff --git a/apps/openmw/mwgui/savegamedialog.cpp b/apps/openmw/mwgui/savegamedialog.cpp index c4d608443..ba95cfc64 100644 --- a/apps/openmw/mwgui/savegamedialog.cpp +++ b/apps/openmw/mwgui/savegamedialog.cpp @@ -30,6 +30,10 @@ #include "../mwstate/character.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrlistbox.hpp" +#endif + #include "confirmationdialog.hpp" namespace MWGui @@ -41,7 +45,6 @@ namespace MWGui , mCurrentSlot(nullptr) { getWidget(mScreenshot, "Screenshot"); - getWidget(mCharacterSelection, "SelectCharacter"); getWidget(mInfoText, "InfoText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); @@ -49,11 +52,28 @@ namespace MWGui getWidget(mSaveList, "SaveList"); getWidget(mSaveNameEdit, "SaveNameEdit"); getWidget(mSpacer, "Spacer"); + getWidget(mCharacterSelection, "SelectCharacter"); + getWidget(mCharacterSelectionButton, "SelectCharacterButton"); + + if (MWBase::Environment::get().getVrMode()) + { +#ifdef USE_OPENXR + mCharacterSelectionListBox = new MWVR::VrListBox(); +#endif + mCharacterSelection->setVisible(false); + mCharacterSelection->setUserString("Hidden", "true"); + } + else + { + mCharacterSelectionButton->setVisible(false); + mCharacterSelectionButton->setUserString("Hidden", "true"); + } + mCharacterSelectionButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelectionButtonClicked); + mCharacterSelection->eventComboChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); + mCharacterSelection->eventComboAccept += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterAccept); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteButtonClicked); - mCharacterSelection->eventComboChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); - mCharacterSelection->eventComboAccept += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterAccept); mSaveList->eventListChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onSlotSelected); mSaveList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SaveGameDialog::onSlotMouseClick); mSaveList->eventListSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onSlotActivated); @@ -147,11 +167,15 @@ namespace MWGui if (mSaving) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveNameEdit); else - MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); + if (MWBase::Environment::get().getVrMode()) + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCharacterSelectionButton); + else + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); center(); mCharacterSelection->setCaption(""); + mCharacterSelectionButton->setCaption(""); mCharacterSelection->removeAllItems(); mCurrentCharacter = nullptr; mCurrentSlot = nullptr; @@ -209,6 +233,8 @@ namespace MWGui mCharacterSelection->setIndexSelected(selectedIndex); if (selectedIndex == MyGUI::ITEM_NONE) mCharacterSelection->setCaption("Select Character ..."); + else + mCharacterSelectionButton->setCaption(mCharacterSelection->getCaption()); fillSaveList(); @@ -218,8 +244,17 @@ namespace MWGui { mSaving = !load; mSaveNameEdit->setVisible(!load); - mCharacterSelection->setUserString("Hidden", load ? "false" : "true"); - mCharacterSelection->setVisible(load); + + if (MWBase::Environment::get().getVrMode()) + { + mCharacterSelectionButton->setUserString("Hidden", load ? "false" : "true"); + mCharacterSelectionButton->setVisible(load); + } + else + { + mCharacterSelection->setUserString("Hidden", load ? "false" : "true"); + mCharacterSelection->setVisible(load); + } mSpacer->setUserString("Hidden", load ? "false" : "true"); mDeleteButton->setUserString("Hidden", load ? "false" : "true"); @@ -244,6 +279,21 @@ namespace MWGui confirmDeleteSave(); } + void SaveGameDialog::onCharacterSelectionButtonClicked(MyGUI::Widget* sender) + { +#ifdef USE_OPENXR + mCharacterSelectionListBox->open(mCharacterSelection, [this](std::size_t index) { + if (index != MyGUI::ITEM_NONE) + { + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); + auto caption = mCharacterSelection->getItemNameAt(index); + mCharacterSelectionButton->setCaption(caption); + onCharacterSelected(mCharacterSelection, index); + } + }); +#endif + } + void SaveGameDialog::onConfirmationGiven() { accept(true); diff --git a/apps/openmw/mwgui/savegamedialog.hpp b/apps/openmw/mwgui/savegamedialog.hpp index c22d86fd1..a9655edc8 100644 --- a/apps/openmw/mwgui/savegamedialog.hpp +++ b/apps/openmw/mwgui/savegamedialog.hpp @@ -11,6 +11,11 @@ namespace MWState struct Slot; } +namespace MWVR +{ + class VrListBox; +} + namespace MWGui { @@ -31,6 +36,7 @@ namespace MWGui void onCancelButtonClicked (MyGUI::Widget* sender); void onOkButtonClicked (MyGUI::Widget* sender); void onDeleteButtonClicked (MyGUI::Widget* sender); + void onCharacterSelectionButtonClicked(MyGUI::Widget* sender); void onCharacterSelected (MyGUI::ComboBox* sender, size_t pos); void onCharacterAccept(MyGUI::ComboBox* sender, size_t pos); // Slot selected (mouse click or arrow keys) @@ -57,6 +63,8 @@ namespace MWGui bool mSaving; MyGUI::ComboBox* mCharacterSelection; + MWVR::VrListBox* mCharacterSelectionListBox; + MyGUI::Button* mCharacterSelectionButton; MyGUI::EditBox* mInfoText; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 4540cc0a7..55803f169 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -31,6 +31,14 @@ #include "confirmationdialog.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vrsession.hpp" +#include "../mwvr/vrviewer.hpp" +#include "../mwvr/vrgui.hpp" +#include "../mwvr/vrinputmanager.hpp" +#endif + namespace { @@ -221,7 +229,11 @@ namespace MWGui } SettingsWindow::SettingsWindow() : +#ifdef USE_OPENXR + WindowBase("openmw_settings_window_vr.layout"), +#else WindowBase("openmw_settings_window.layout"), +#endif mKeyboardMode(true) { bool terrain = Settings::Manager::getBool("distant terrain", "Terrain"); @@ -251,6 +263,12 @@ namespace MWGui getWidget(mLightsResetButton, "LightsResetButton"); getWidget(mMaxLights, "MaxLights"); + if (MWBase::Environment::get().getVrMode()) + { + getWidget(mVRMirrorTextureEye, "VRMirrorTextureEye"); + getWidget(mVRLeftHudPosition, "VRLeftHudPosition"); + } + #ifndef WIN32 // hide gamma controls since it currently does not work under Linux MyGUI::ScrollBar *gammaSlider; @@ -282,6 +300,12 @@ namespace MWGui mKeyboardSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onKeyboardSwitchClicked); mControllerSwitch->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onControllerSwitchClicked); + if (MWBase::Environment::get().getVrMode()) + { + mVRMirrorTextureEye->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onVRMirrorTextureEyeChanged); + mVRLeftHudPosition->eventComboChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onVRLeftHudPositionChanged); + } + center(); mResetControlsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindings); @@ -312,6 +336,19 @@ namespace MWGui std::string tmip = Settings::Manager::getString("texture mipmap", "General"); mTextureFilteringButton->setCaption(textureMipmappingToStr(tmip)); + if (MWBase::Environment::get().getVrMode()) + { + std::string mirrorTextureEye = Settings::Manager::getString("mirror texture eye", "VR"); + for (unsigned i = 0; i < mVRMirrorTextureEye->getItemCount(); i++) + if (Misc::StringUtils::ciEqual(mirrorTextureEye, mVRMirrorTextureEye->getItem(i))) + mVRMirrorTextureEye->setIndexSelected(i); + + std::string leftHandHudPosition = Settings::Manager::getString("left hand hud position", "VR"); + for (unsigned i = 0; i < mVRLeftHudPosition->getItemCount(); i++) + if (Misc::StringUtils::ciEqual(leftHandHudPosition, mVRLeftHudPosition->getItem(i))) + mVRLeftHudPosition->setIndexSelected(i); + } + int waterTextureSize = Settings::Manager::getInt("rtt size", "Water"); if (waterTextureSize >= 512) mWaterTextureSize->setIndexSelected(0); @@ -392,6 +429,20 @@ namespace MWGui } } + void SettingsWindow::onVRMirrorTextureEyeChanged(MyGUI::ComboBox* _sender, size_t pos) + { + auto setting = Misc::StringUtils::lowerCase(_sender->getItem(pos)); + Settings::Manager::setString("mirror texture eye", "VR", setting); + apply(); + } + + void SettingsWindow::onVRLeftHudPositionChanged(MyGUI::ComboBox* _sender, size_t pos) + { + auto setting = Misc::StringUtils::lowerCase(_sender->getItem(pos)); + Settings::Manager::setString("left hand hud position", "VR", setting); + apply(); + } + void SettingsWindow::onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos) { int size = 0; @@ -590,6 +641,15 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->processChangedSettings(changed); MWBase::Environment::get().getInputManager()->processChangedSettings(changed); MWBase::Environment::get().getMechanicsManager()->processChangedSettings(changed); +#ifdef USE_OPENXR + if (MWBase::Environment::get().getVrMode()) + { + MWVR::Environment::get().getSession()->processChangedSettings(changed); + MWVR::Environment::get().getTrackingManager()->processChangedSettings(changed); + MWVR::Environment::get().getViewer()->processChangedSettings(changed); + MWVR::Environment::get().getGUIManager()->processChangedSettings(changed); + } +#endif Settings::Manager::resetPendingChanges(); } diff --git a/apps/openmw/mwgui/settingswindow.hpp b/apps/openmw/mwgui/settingswindow.hpp index 9c28733f9..6f405b5e9 100644 --- a/apps/openmw/mwgui/settingswindow.hpp +++ b/apps/openmw/mwgui/settingswindow.hpp @@ -22,6 +22,10 @@ namespace MWGui MyGUI::TabControl* mSettingsTab; MyGUI::Button* mOkButton; + // VR + MyGUI::ComboBox* mVRLeftHudPosition; + MyGUI::ComboBox* mVRMirrorTextureEye; + // graphics MyGUI::ListBox* mResolutionList; MyGUI::Button* mFullscreenButton; @@ -53,6 +57,9 @@ namespace MWGui void onResolutionCancel(); void highlightCurrentResolution(); + void onVRMirrorTextureEyeChanged(MyGUI::ComboBox* _sender, size_t pos); + void onVRLeftHudPositionChanged(MyGUI::ComboBox* _sender, size_t pos); + void onWaterTextureSizeChanged(MyGUI::ComboBox* _sender, size_t pos); void onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos); diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 405abfbae..ee51e1e7b 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -65,8 +65,14 @@ namespace MWGui int w=2; + for(auto rit = effects.rbegin(); rit != effects.rend(); rit++) + { + auto& effectInfoPair = *rit; +#if 0 + // in VR mode, the effect box grows to the right so we want to invert the order to avoid reordering effects. for (auto& effectInfoPair : effects) { +#endif const int effectId = effectInfoPair.first; const ESM::MagicEffect* effect = MWBase::Environment::get().getWorld ()->getStore ().get().find(effectId); @@ -194,7 +200,10 @@ namespace MWGui s = 0; int diff = parent->getWidth() - s; parent->setSize(s, parent->getHeight()); +#ifndef USE_OPENXR + // in VR mode, the effect box grows to the right and does not need repositioning parent->setPosition(parent->getLeft()+diff, parent->getTop()); +#endif } // hide inactive effects diff --git a/apps/openmw/mwgui/spellwindow.cpp b/apps/openmw/mwgui/spellwindow.cpp index b13737fa1..1c3bb3216 100644 --- a/apps/openmw/mwgui/spellwindow.cpp +++ b/apps/openmw/mwgui/spellwindow.cpp @@ -40,7 +40,11 @@ namespace MWGui { SpellWindow::SpellWindow(DragAndDrop* drag) +#ifdef USE_OPENXR + : WindowPinnableBase("openmw_spell_window_vr.layout") +#else : WindowPinnableBase("openmw_spell_window.layout") +#endif , NoDrop(drag, mMainWidget) , mSpellView(nullptr) , mUpdateTimer(0.0f) diff --git a/apps/openmw/mwgui/statswindow.cpp b/apps/openmw/mwgui/statswindow.cpp index 8e6f95129..7ff0c866d 100644 --- a/apps/openmw/mwgui/statswindow.cpp +++ b/apps/openmw/mwgui/statswindow.cpp @@ -26,7 +26,11 @@ namespace MWGui { StatsWindow::StatsWindow (DragAndDrop* drag) +#ifdef USE_OPENXR + : WindowPinnableBase("openmw_stats_window_vr.layout") +#else : WindowPinnableBase("openmw_stats_window.layout") +#endif , NoDrop(drag, mMainWidget) , mSkillView(nullptr) , mMajorSkills() diff --git a/apps/openmw/mwgui/statswindow.hpp b/apps/openmw/mwgui/statswindow.hpp index bf78cde34..08fa5d86e 100644 --- a/apps/openmw/mwgui/statswindow.hpp +++ b/apps/openmw/mwgui/statswindow.hpp @@ -46,7 +46,7 @@ namespace MWGui void setExpelled (const std::set& expelled); void setBirthSign (const std::string &signId); - void onWindowResize(MyGUI::Window* window); + void onWindowResize(MyGUI::Window* window) override; void onMouseWheel(MyGUI::Widget* _sender, int _rel); MyGUI::Widget* mLeftPane; diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index a821d0106..356945dd0 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -30,7 +30,11 @@ namespace MWGui std::string ToolTips::sSchoolNames[] = {"#{sSchoolAlteration}", "#{sSchoolConjuration}", "#{sSchoolDestruction}", "#{sSchoolIllusion}", "#{sSchoolMysticism}", "#{sSchoolRestoration}"}; ToolTips::ToolTips() : +#ifdef USE_OPENXR + Layout("openmw_tooltips_vr.layout") +#else Layout("openmw_tooltips.layout") +#endif , mFocusToolTipX(0.0) , mFocusToolTipY(0.0) , mHorizontalScrollIndex(0) @@ -52,7 +56,9 @@ namespace MWGui mDynamicToolTipBox->setNeedMouseFocus(false); mMainWidget->setNeedMouseFocus(false); - mDelay = Settings::Manager::getFloat("tooltip delay", "GUI"); + // Tooltip delay is not useful in vr as a player cannot be perfectly still. + if (!MWBase::Environment::get().getVrMode()) + mDelay = Settings::Manager::getFloat("tooltip delay", "GUI"); mRemainingDelay = mDelay; for (unsigned int i=0; i < mMainWidget->getChildCount(); ++i) diff --git a/apps/openmw/mwgui/tradewindow.cpp b/apps/openmw/mwgui/tradewindow.cpp index 422b17186..80b694bcd 100644 --- a/apps/openmw/mwgui/tradewindow.cpp +++ b/apps/openmw/mwgui/tradewindow.cpp @@ -58,7 +58,11 @@ namespace namespace MWGui { TradeWindow::TradeWindow() +#ifdef USE_OPENXR + : WindowBase("openmw_trade_window_vr.layout") +#else : WindowBase("openmw_trade_window.layout") +#endif , mSortModel(nullptr) , mTradeModel(nullptr) , mItemToSell(-1) diff --git a/apps/openmw/mwgui/windowbase.cpp b/apps/openmw/mwgui/windowbase.cpp index 84e557fcd..0c216b6de 100644 --- a/apps/openmw/mwgui/windowbase.cpp +++ b/apps/openmw/mwgui/windowbase.cpp @@ -12,6 +12,11 @@ #include "draganddrop.hpp" #include "exposedwindow.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrgui.hpp" +#include "../mwvr/vrenvironment.hpp" +#endif + using namespace MWGui; WindowBase::WindowBase(const std::string& parLayout) @@ -55,6 +60,19 @@ void WindowBase::setVisible(bool visible) onOpen(); else if (wasVisible) onClose(); + +#ifdef USE_OPENXR + // Check that onOpen/onClose didn't reverse the change before forwarding it + // to the VR GUI manager. + if (this->isVisible() == visible) + { + auto* vrGUIManager = MWVR::Environment::get().getGUIManager(); + if (!vrGUIManager) + // May end up here before before rendering has been fully set up + return; + vrGUIManager->setVisible(this, visible); + } +#endif } bool WindowBase::isVisible() @@ -109,26 +127,42 @@ void NoDrop::onFrame(float dt) MyGUI::IntPoint mousePos = MyGUI::InputManager::getInstance().getMousePosition(); +#ifdef USE_OPENXR + // Since VR mode stretches some windows to full screen, the usual outside condition + // won't work + mTransparent = false; +#endif if (mDrag->mIsOnDragAndDrop) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getMouseFocusWidget(); while (focus && focus != mWidget) + { focus = focus->getParent(); + } if (focus == mWidget) + { mTransparent = true; + } } if (!mWidget->getAbsoluteCoord().inside(mousePos)) mTransparent = false; if (mTransparent) { +#ifndef USE_OPENXR + // These makes focus null, which messes up the logic for VR + // since i reset mTransparent to false every update. + // TODO: Is there a cleaner way? mWidget->setNeedMouseFocus(false); // Allow click-through +#endif setAlpha(std::max(0.13f, mWidget->getAlpha() - dt*5)); } else { - mWidget->setNeedMouseFocus(true); +#ifndef USE_OPENXR + mWidget->setNeedMouseFocus(true); // Allow click-through +#endif setAlpha(std::min(1.0f, mWidget->getAlpha() + dt*5)); } } diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 90ef2118d..9427621b9 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -47,6 +47,9 @@ namespace MWGui /// Called when GUI viewport changes size virtual void onResChange(int width, int height) {} + /// Called when Window widget changes in size + virtual void onWindowResize(MyGUI::Window* window) {} + protected: virtual void onTitleDoubleClicked(); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 3a63010c9..0df71b6e9 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -131,6 +131,16 @@ #include "keyboardnavigation.hpp" #include "resourceskin.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrmetamenu.hpp" +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vrgui.hpp" +#include "../mwvr/vrvirtualkeyboard.hpp" +#include "../mwvr/vrviewer.hpp" +#include "../mwvr/vrsession.hpp" +#include "../mwvr/vrtracking.hpp" +#endif + namespace MWGui { WindowManager::WindowManager( @@ -174,6 +184,8 @@ namespace MWGui , mScreenFader(nullptr) , mDebugWindow(nullptr) , mJailScreen(nullptr) + , mVrMetaMenu(nullptr) + , mVirtualKeyboardManager(nullptr) , mTranslationDataStorage (translationDataStorage) , mCharGen(nullptr) , mInputBlocker(nullptr) @@ -184,6 +196,7 @@ namespace MWGui , mHudEnabled(true) , mCursorVisible(true) , mCursorActive(true) + , mVideoEnabled(false) , mPlayerBounty(-1) , mGui(nullptr) , mGuiModes() @@ -202,6 +215,11 @@ namespace MWGui mGuiPlatform = new osgMyGUI::Platform(viewer, guiRoot, resourceSystem->getImageManager(), mScalingFactor); mGuiPlatform->initialise(resourcePath, (boost::filesystem::path(logpath) / "MyGUI.log").generic_string()); + +#ifdef USE_OPENXR + mGuiPlatform->getRenderManagerPtr()->setViewSize(1024, 1024); +#endif + mGui = new MyGUI::Gui; mGui->initialise(""); @@ -236,7 +254,17 @@ namespace MWGui MyGUI::FactoryManager::getInstance().registerFactory("Resource", "ResourceImageSetPointer"); MyGUI::FactoryManager::getInstance().registerFactory("Resource", "AutoSizedResourceSkin"); + +#ifdef USE_OPENXR + if (MWBase::Environment::get().getVrMode()) + MWVR::VRGUIManager::registerMyGUIFactories(); +#endif + +#ifdef USE_OPENXR + MyGUI::ResourceManager::getInstance().load("core_vr.xml"); +#else MyGUI::ResourceManager::getInstance().load("core.xml"); +#endif WindowManager::loadUserFonts(); bool keyboardNav = Settings::Manager::getBool("keyboard navigation", "GUI"); @@ -269,7 +297,7 @@ namespace MWGui mVideoBackground->setNeedMouseFocus(true); mVideoBackground->setNeedKeyFocus(true); - mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default); + mVideoWidget = mVideoBackground->createWidgetReal("ImageBox", 0,0,1,1, MyGUI::Align::Default, "InputBlocker"); mVideoWidget->setNeedMouseFocus(true); mVideoWidget->setNeedKeyFocus(true); mVideoWidget->setVFS(resourceSystem->getVFS()); @@ -283,7 +311,7 @@ namespace MWGui mShowOwned = Settings::Manager::getInt("show owned", "Game"); - mVideoWrapper = new SDLUtil::VideoWrapper(window, viewer); + mVideoWrapper = new SDLUtil::VideoWrapper(window, viewer, MWBase::Environment::get().getVrMode() != true); mVideoWrapper->setGammaContrast(Settings::Manager::getFloat("gamma", "Video"), Settings::Manager::getFloat("contrast", "Video")); @@ -305,6 +333,14 @@ namespace MWGui mDragAndDrop = new DragAndDrop(); +#ifdef USE_OPENXR + mVrMetaMenu = new MWVR::VrMetaMenu(w, h); + mWindows.push_back(mVrMetaMenu); + mGuiModeStates[GM_VrMetaMenu] = GuiModeState(mVrMetaMenu); + + mVirtualKeyboardManager = new MWVR::VirtualKeyboardManager; +#endif + Recharge* recharge = new Recharge(); mGuiModeStates[GM_Recharge] = GuiModeState(recharge); mWindows.push_back(recharge); @@ -582,18 +618,28 @@ namespace MWGui void WindowManager::enableScene(bool enable) { + unsigned int disablemask = MWRender::Mask_GUI|MWRender::Mask_PreCompile; + + // VR mode needs to render the 3D gui + if (MWBase::Environment::get().getVrMode()) + disablemask = MWRender::Mask_Pointer | MWRender::Mask_3DGUI | MWRender::Mask_PreCompile | MWRender::Mask_RenderToTexture; + if (!enable && mViewer->getCamera()->getCullMask() != disablemask) { mOldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask(); mOldCullMask = mViewer->getCamera()->getCullMask(); mViewer->getUpdateVisitor()->setTraversalMask(disablemask); mViewer->getCamera()->setCullMask(disablemask); + mViewer->getCamera()->setCullMaskLeft(disablemask); + mViewer->getCamera()->setCullMaskRight(disablemask); } else if (enable && mViewer->getCamera()->getCullMask() == disablemask) { mViewer->getUpdateVisitor()->setTraversalMask(mOldUpdateMask); mViewer->getCamera()->setCullMask(mOldCullMask); + mViewer->getCamera()->setCullMaskLeft(mOldCullMask); + mViewer->getCamera()->setCullMaskRight(mOldCullMask); } } @@ -732,6 +778,8 @@ namespace MWGui } } + mVideoEnabled = false; + popGuiMode(); } @@ -766,11 +814,7 @@ namespace MWGui if (!mWindowVisible) std::this_thread::sleep_for(std::chrono::milliseconds(5)); else - { - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); - } + viewerTraversals(false); // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() @@ -1173,6 +1217,9 @@ namespace MWGui void WindowManager::windowResized(int x, int y) { +#ifdef USE_OPENXR + return; +#endif // Note: this is a side effect of resolution change or window resize. // There is no need to track these changes. Settings::Manager::setInt("resolution x", "Video", x); @@ -1534,6 +1581,11 @@ namespace MWGui updateVisible(); } + DragAndDrop& WindowManager::getDragAndDrop(void) + { + return *mDragAndDrop; + } + void WindowManager::forceHide(GuiWindow wnd) { mForceHidden = (GuiWindow)(mForceHidden | wnd); @@ -1886,6 +1938,7 @@ namespace MWGui void WindowManager::playVideo(const std::string &name, bool allowSkipping) { + mVideoEnabled = true; mVideoWidget->playVideo("video\\" + name); mVideoWidget->eventKeyButtonPressed.clear(); @@ -1906,6 +1959,11 @@ namespace MWGui mVideoBackground->setVisible(true); +#ifdef USE_OPENXR + auto* vrGuiManager = MWVR::Environment::get().getGUIManager(); + vrGuiManager->insertLayer(mVideoBackground->getLayer()->getName()); +#endif + bool cursorWasVisible = mCursorVisible; setCursorVisible(false); @@ -1915,7 +1973,7 @@ namespace MWGui ); Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); - while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) + while (mVideoEnabled && mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()).count(); @@ -1931,9 +1989,7 @@ namespace MWGui if (mVideoWidget->isPaused()) mVideoWidget->resume(); - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); + viewerTraversals(false); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. @@ -1953,7 +2009,16 @@ namespace MWGui // Restore normal rendering updateVisible(); +#ifdef USE_OPENXR + vrGuiManager->removeLayer(mVideoBackground->getLayer()->getName()); +#endif mVideoBackground->setVisible(false); + mVideoEnabled = false; + } + + bool WindowManager::isPlayingVideo(void) const + { + return mVideoEnabled; } void WindowManager::sizeVideo(int screenWidth, int screenHeight) @@ -2370,6 +2435,15 @@ namespace MWGui return MyGUI::InputManager::getInstance().injectKeyRelease(key); } + void WindowManager::viewerTraversals(bool updateWindowManager) + { + mViewer->eventTraversal(); + mViewer->updateTraversal(); + if (updateWindowManager) + MWBase::Environment::get().getWorld()->updateWindowManager(); + mViewer->renderingTraversals(); + } + void WindowManager::GuiModeState::update(bool visible) { for (unsigned int i=0; i mWorkQueue; @@ -535,6 +544,9 @@ namespace MWGui ScreenFader* mScreenFader; DebugWindow* mDebugWindow; JailScreen* mJailScreen; + MWVR::VrMetaMenu* mVrMetaMenu; + + Gui::VirtualKeyboardManager* mVirtualKeyboardManager; /* Start of tes3mp addition @@ -562,6 +574,7 @@ namespace MWGui bool mHudEnabled; bool mCursorVisible; bool mCursorActive; + bool mVideoEnabled; int mPlayerBounty; diff --git a/apps/openmw/mwinput/bindingsmanager.cpp b/apps/openmw/mwinput/bindingsmanager.cpp index 51dcd46da..80b9edacb 100644 --- a/apps/openmw/mwinput/bindingsmanager.cpp +++ b/apps/openmw/mwinput/bindingsmanager.cpp @@ -16,6 +16,8 @@ End of tes3mp addition */ +#include + #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -59,6 +61,12 @@ namespace MWInput } }; + ICS::InputControlSystem& + BindingsManager::ics() + { + return *mInputBinder; + } + class BindingsListener : public ICS::ChannelListener, public ICS::DetectingBindingListener diff --git a/apps/openmw/mwinput/bindingsmanager.hpp b/apps/openmw/mwinput/bindingsmanager.hpp index 74416d3c7..0b5e0701e 100644 --- a/apps/openmw/mwinput/bindingsmanager.hpp +++ b/apps/openmw/mwinput/bindingsmanager.hpp @@ -7,6 +7,11 @@ #include +namespace ICS +{ +class InputControlSystem; +} + namespace MWInput { class BindingsListener; @@ -62,6 +67,8 @@ namespace MWInput void actionValueChanged(int action, float currentValue, float previousValue); + ICS::InputControlSystem& ics(); + private: void setupSDLKeyMappings(); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index f930836d1..5ad392bab 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -95,14 +95,16 @@ namespace MWInput void executeAction(int action) override; bool controlsDisabled() override { return mControlsDisabled; } + void applyHapticsLeftHand(float intensity) override {}; + void applyHapticsRightHand(float intensity) override {}; - private: + protected: void convertMousePosForMyGUI(int& x, int& y); void handleGuiArrowKey(int action); - void quickKey(int index); - void showQuickKeysMenu(); + //void quickKey(int index); + //void showQuickKeysMenu(); void loadKeyDefaults(bool force = false); void loadControllerDefaults(bool force = false); diff --git a/apps/openmw/mwinput/mousemanager.cpp b/apps/openmw/mwinput/mousemanager.cpp index cf151dfac..7b6d00082 100644 --- a/apps/openmw/mwinput/mousemanager.cpp +++ b/apps/openmw/mwinput/mousemanager.cpp @@ -63,6 +63,9 @@ namespace MWInput void MouseManager::mouseMoved(const SDLUtil::MouseMotionEvent &arg) { + if (MWBase::Environment::get().getVrMode()) + return; + mBindingsManager->mouseMoved(arg); MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); @@ -235,6 +238,8 @@ namespace MWInput void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove) { + if (MWBase::Environment::get().getVrMode()) + return; mGuiCursorX += xMove; mGuiCursorY += yMove; mMouseWheel += mouseWheelMove; @@ -251,4 +256,11 @@ namespace MWInput float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mInputWrapper->warpMouse(static_cast(mGuiCursorX*uiScale), static_cast(mGuiCursorY*uiScale)); } + + void MouseManager::setMousePosition(int x, int y) + { + float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); + mGuiCursorX = x / uiScale; + mGuiCursorY = y / uiScale; + } } diff --git a/apps/openmw/mwinput/mousemanager.hpp b/apps/openmw/mwinput/mousemanager.hpp index 000e7cd0b..a1170e313 100644 --- a/apps/openmw/mwinput/mousemanager.hpp +++ b/apps/openmw/mwinput/mousemanager.hpp @@ -38,6 +38,9 @@ namespace MWInput void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } + // Used to override mouse position when using controllers not through SDL, such as OpenXR. + void setMousePosition(int x, int y); + private: bool mInvertX; bool mInvertY; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 068929ce6..f3f241636 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -57,6 +57,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" +#include "../mwrender/npcanimation.hpp" #include "aicombataction.hpp" #include "movement.hpp" @@ -66,6 +67,12 @@ #include "actorutil.hpp" #include "spellcasting.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vranimation.hpp" +#include "../mwvr/vrutil.hpp" +#endif + namespace { @@ -1106,7 +1113,7 @@ void CharacterController::handleTextKey(const std::string &groupname, SceneUtil: else if (groupname == "attack3" || groupname == "swimattack3") mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); else - mPtr.getClass().hit(mPtr, mAttackStrength); + mPtr.getClass().hit(mPtr, mAttackStrength, -1); } else if (!groupname.empty() && (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0) @@ -1720,9 +1727,13 @@ bool CharacterController::updateWeaponState(CharacterState& idle) { MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWWorld::Ptr item = *weapon; + std::string resultMessage, resultSound; // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. +#ifdef USE_OPENXR + MWWorld::Ptr target = MWVR::Util::getWeaponTarget().first; +#else MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); - std::string resultMessage, resultSound; +#endif if(!target.isEmpty()) { @@ -2663,6 +2674,18 @@ void CharacterController::update(float duration) mSkipAnim = false; mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); + +#ifdef USE_OPENXR + if (isPlayer) + { + auto disabled = MWBase::Environment::get().getWorld()->getPlayer().isDisabled(); + auto animation = static_cast(mAnimation); + if (disabled) + animation->setViewMode(MWRender::NpcAnimation::VM_VRNormal); + else + animation->setViewMode(MWRender::NpcAnimation::VM_VRFirstPerson); + } +#endif } void CharacterController::persistAnimationState() diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index c563da0f7..dba92738a 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -294,7 +294,7 @@ namespace MWMechanics End of tes3mp addition */ - victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false); + victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, attackStrength); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } @@ -354,7 +354,7 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1, victim); } - victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); + victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true, attackStrength); } /* Start of tes3mp addition diff --git a/apps/openmw/mwphysics/movementsolver.cpp b/apps/openmw/mwphysics/movementsolver.cpp index fd0e090fc..07dff6932 100644 --- a/apps/openmw/mwphysics/movementsolver.cpp +++ b/apps/openmw/mwphysics/movementsolver.cpp @@ -14,6 +14,15 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/refdata.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrsession.hpp" +#include "../mwvr/vrcamera.hpp" +#include "../mwvr/vrenvironment.hpp" +#include "../mwrender/renderingmanager.hpp" +#include "../mwworld/player.hpp" +#endif +#include "../mwmechanics/actorutil.hpp" + #include "actor.hpp" #include "collisiontype.hpp" #include "constants.hpp" @@ -119,7 +128,8 @@ namespace MWPhysics WorldFrameData& worldData) { auto* physicActor = actor.mActorRaw; - const ESM::Position& refpos = actor.mRefpos; + ESM::Position refpos = actor.mRefpos; + // Early-out for totally static creatures // (Not sure if gravity should still apply?) { @@ -128,6 +138,36 @@ namespace MWPhysics return; } + const bool isPlayer = (physicActor->getPtr() == MWMechanics::getPlayer()); + auto* world = MWBase::Environment::get().getWorld(); + + // In VR, player should move according to current direction of + // a selected limb, rather than current orientation of camera. +#ifdef USE_OPENXR + // Regarding this and the duplicate movement solver later in this method: + // As my two edits in this code are obviously hacks, I could use feedback on how i could implement + // VR movement mechanics as not-a-hack. This hack, for instance, does not trigger movement animations + // and will obviously be a poor fit for a future merge with tes3mp. + + // The exact mechanics are: + // 1. When moving with the controller, the player moves in the direction he is currently pointing his left controller. + // 2. The game should seek to eliminate all distance between the player character and the player's position within VR, + // without teleporting the player or ignoring collisions. + + // I assume (1.) is easily solved, i just haven't taken the effort to study openmw's code enough. + // But 2. is not so obvious. I guess it's doable if i compute the direction between current position and the player's + // position in the VR stage, and just let it catch up at the character's own move speed, but it still needs to reach the position as exactly as possible. + if (isPlayer) + { + auto tm = MWVR::Environment::get().getTrackingManager(); + float pitch = 0.f; + float yaw = 0.f; + tm->movementAngles(yaw, pitch); + refpos.rot[0] += pitch; + refpos.rot[2] += yaw; + } +#endif + // Reset per-frame data physicActor->setWalkingOnWater(false); // Anything to collide with? @@ -148,7 +188,7 @@ namespace MWPhysics osg::Vec3f halfExtents = physicActor->getHalfExtents(); actor.mPosition.z() += halfExtents.z(); // vanilla-accurate - static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); + static const float fSwimHeightScale = world->getStore().get().find("fSwimHeightScale")->mValue.getFloat(); float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); ActorTracer tracer; @@ -184,13 +224,103 @@ namespace MWPhysics { osg::Vec3f stormDirection = worldData.mStormDirection; float angleDegrees = osg::RadiansToDegrees(std::acos(stormDirection * velocity / (stormDirection.length() * velocity.length()))); - static const float fStromWalkMult = MWBase::Environment::get().getWorld()->getStore().get().find("fStromWalkMult")->mValue.getFloat(); + static const float fStromWalkMult = world->getStore().get().find("fStromWalkMult")->mValue.getFloat(); velocity *= 1.f-(fStromWalkMult * (angleDegrees/180.f)); } Stepper stepper(collisionWorld, colobj); osg::Vec3f origVelocity = velocity; osg::Vec3f newPosition = actor.mPosition; + +//#ifdef USE_OPENXR +// // Catch the player character up to the real world position of the player. +// // But only if play is not seated. +// // TODO: This solution is a hack. +// if (isPlayer) +// { +// bool shouldMove = true; +// if (session && session->seatedPlay()) +// shouldMove = false; +// if (world->getPlayer().isDisabled()) +// shouldMove = false; +// +// if (shouldMove) +// { +// auto* inputManager = reinterpret_cast(MWBase::Environment::get().getWorld()->getRenderingManager().getCamera()); +// +// osg::Vec3 headOffset = inputManager->headOffset(); +// osg::Vec3 trackingOffset = headOffset; +// // Player's tracking height should not affect character position +// trackingOffset.z() = 0; +// +// float remainingTime = time; +// bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying; +// float remainder = 1.f; +// +// for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f && remainder > 0.01; ++iterations) +// { +// osg::Vec3 toMove = trackingOffset * remainder; +// osg::Vec3 nextpos = newPosition + toMove; +// +// if ((newPosition - nextpos).length2() > 0.0001) +// { +// // trace to where character would go if there were no obstructions +// tracer.doTrace(colobj, newPosition, nextpos, collisionWorld); +// +// // check for obstructions +// if (tracer.mFraction >= 1.0f) +// { +// newPosition = tracer.mEndPos; // ok to move, so set newPosition +// remainder = 0.f; +// break; +// } +// } +// else +// { +// // The current position and next position are nearly the same, so just exit. +// // Note: Bullet can trigger an assert in debug modes if the positions +// // are the same, since that causes it to attempt to normalize a zero +// // length vector (which can also happen with nearly identical vectors, since +// // precision can be lost due to any math Bullet does internally). Since we +// // aren't performing any collision detection, we want to reject the next +// // position, so that we don't slowly move inside another object. +// remainder = 0.f; +// break; +// } +// +// if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel) +// seenGround = true; +// +// // We are touching something. +// if (tracer.mFraction < 1E-9f) +// { +// // Try to separate by backing off slighly to unstuck the solver +// osg::Vec3f backOff = (newPosition - tracer.mHitPoint) * 1E-2f; +// newPosition += backOff; +// } +// +// // We hit something. Check if we can step up. +// float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z(); +// osg::Vec3f oldPosition = newPosition; +// bool result = false; +// if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject)) +// { +// // Try to step up onto it. +// // NOTE: stepMove does not allow stepping over, modifies newPosition if successful +// result = stepper.step(newPosition, toMove, remainingTime, seenGround, iterations == 0); +// remainder = remainingTime / time; +// } +// } +// +// // Try not to lose any tracking +// osg::Vec3 moved = newPosition - actor.mPosition; +// headOffset.x() -= moved.x(); +// headOffset.y() -= moved.y(); +// inputManager->setHeadOffset(headOffset); +// } +// } +//#endif + /* * A loop to find newPosition using tracer, if successful different from the starting position. * nextpos is the local variable used to find potential newPosition, using velocity and remainingTime diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index b578ee25b..473721d86 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -42,6 +42,7 @@ #include "../mwworld/cellstore.hpp" #include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority +#include "../mwmechanics/actorutil.hpp" #include "vismask.hpp" #include "util.hpp" @@ -1052,8 +1053,56 @@ namespace MWRender return mNodeMap; } + static bool vrOverride(const std::string& groupname, const std::string& bone) + { +#ifdef USE_OPENXR + // 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. + // Add any bone+groupname pair that is messing with Vr comfort here. + using Overrides = std::set; + using GroupOverrides = std::map; + static GroupOverrides sVrOverrides = + { + { + "crossbow", + { + "weapon bone" + } + }, + { + "throwweapon", + { + "weapon bone" + } + }, + { + "bowandarrow", + { + "weapon bone" + } + }, + }; + + bool override = false; + auto find = sVrOverrides.find(groupname); + if (find != sVrOverrides.end()) + { + override = !!find->second.count(bone); + } + + return override; +#else + (void)bone; + (void)groupname; + return false; +#endif + } + void Animation::resetActiveGroups() { + const bool isPlayer = (mPtr == MWMechanics::getPlayer()); + // remove all previous external controllers from the scene graph for (auto it = mActiveControllers.begin(); it != mActiveControllers.end(); ++it) { @@ -1092,11 +1141,16 @@ namespace MWRender for (AnimSource::ControllerMap::iterator it = animsrc->mControllerMap[blendMask].begin(); it != animsrc->mControllerMap[blendMask].end(); ++it) { osg::ref_ptr node = getNodeMap().at(it->first); // this should not throw, we already checked for the node existing in addAnimSource - - node->addUpdateCallback(it->second); + if(!isPlayer || !vrOverride(active->first, it->first)) + node->addUpdateCallback(it->second); mActiveControllers.emplace_back(node, it->second); - if (blendMask == 0 && node == mAccumRoot) + if (blendMask == 0 && node == mAccumRoot +#ifdef USE_OPENXR + // TODO: Little hack to keep certain animations from wobbling the camera in VR + && (!isPlayer) +#endif + ) { mAccumCtrl = it->second; diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 213a4f704..df9c2d5b4 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -448,7 +448,7 @@ public: void disable(const std::string &groupname); /** Retrieves the velocity (in units per second) that the animation will move. */ - float getVelocity(const std::string &groupname) const; + virtual float getVelocity(const std::string &groupname) const; virtual osg::Vec3f runAnimation(float duration); diff --git a/apps/openmw/mwrender/camera.cpp b/apps/openmw/mwrender/camera.cpp index 3e5d1d0b3..1a954b8cc 100644 --- a/apps/openmw/mwrender/camera.cpp +++ b/apps/openmw/mwrender/camera.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -126,6 +127,16 @@ namespace MWRender return position; } + osg::Camera* Camera::getOsgCamera() + { + return mCamera; + } + + void Camera::updateCamera() + { + updateCamera(mCamera); + } + osg::Vec3d Camera::getFocalPointOffset() const { osg::Vec3d offset(0, 0, 10.f); @@ -147,12 +158,19 @@ namespace MWRender camera = focal + offset; } + void Camera::getOrientation(osg::Quat& orientation) const + { + orientation = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1)); + } + void Camera::updateCamera(osg::Camera *cam) { osg::Vec3d focal, position; getPosition(focal, position); - osg::Quat orient = osg::Quat(mRoll, osg::Vec3d(0, 1, 0)) * osg::Quat(mPitch, osg::Vec3d(1, 0, 0)) * osg::Quat(mYaw, osg::Vec3d(0, 0, 1)); + osg::Quat orient; + getOrientation(orient); + osg::Vec3d forward = orient * osg::Vec3d(0,1,0); osg::Vec3d up = orient * osg::Vec3d(0,0,1); @@ -185,8 +203,10 @@ namespace MWRender toggleViewMode(); } - void Camera::rotateCamera(float pitch, float yaw, bool adjust) + void Camera::rotateCamera(float pitch, float roll, float yaw, bool adjust) { + (void)roll; + if (adjust) { pitch += getPitch(); @@ -223,7 +243,7 @@ namespace MWRender && (mFirstPersonView || mShowCrosshairInThirdPersonMode)); if(mMode == Mode::Vanity) - rotateCamera(0.f, osg::DegreesToRadians(3.f * duration), true); + rotateCamera(0.f, 0.f, osg::DegreesToRadians(3.f * duration), true); if (isFirstPerson() && mHeadBobbingEnabled) updateHeadBobbing(duration); @@ -503,7 +523,7 @@ namespace MWRender else mHeightScale = 1.f; } - rotateCamera(getPitch(), getYaw(), false); + rotateCamera(getPitch(), 0.f, getYaw(), false); } void Camera::applyDeferredPreviewRotationToPlayer(float dt) diff --git a/apps/openmw/mwrender/camera.hpp b/apps/openmw/mwrender/camera.hpp index 9e2b608df..6c0f8caac 100644 --- a/apps/openmw/mwrender/camera.hpp +++ b/apps/openmw/mwrender/camera.hpp @@ -14,6 +14,7 @@ namespace osg class Camera; class NodeCallback; class Node; + class Quat; } namespace MWRender @@ -26,7 +27,7 @@ namespace MWRender public: enum class Mode { Normal, Vanity, Preview, StandingPreview }; - private: + protected: MWWorld::Ptr mTrackingPtr; osg::ref_ptr mTrackingNode; float mHeightScale; @@ -92,7 +93,7 @@ namespace MWRender public: Camera(osg::Camera* camera); - ~Camera(); + virtual ~Camera(); /// Attach camera to object void attachTo(const MWWorld::Ptr &ptr) { mTrackingPtr = ptr; } @@ -100,20 +101,23 @@ namespace MWRender void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; } void setFocalPointTargetOffset(osg::Vec2d v); - void instantTransition(); + virtual void instantTransition(); void enableDynamicCameraDistance(bool v) { mDynamicCameraDistanceEnabled = v; } void enableCrosshairInThirdPersonMode(bool v) { mShowCrosshairInThirdPersonMode = v; } /// Update the view matrix of \a cam - void updateCamera(osg::Camera* cam); + virtual void updateCamera(osg::Camera* cam); + + /// Update the view matrix of the current camera + virtual void updateCamera(); /// Reset to defaults - void reset(); + virtual void reset(); /// Set where the camera is looking at. Uses Morrowind (euler) angles /// \param rot Rotation angles in radians - void rotateCamera(float pitch, float yaw, bool adjust); - void rotateCameraToTrackingPtr(); + virtual void rotateCamera(float pitch, float roll, float yaw, bool adjust); + virtual void rotateCameraToTrackingPtr(); float getYaw() const { return mYaw; } void setYaw(float angle); @@ -122,10 +126,10 @@ namespace MWRender void setPitch(float angle); /// @param Force view mode switch, even if currently not allowed by the animation. - void toggleViewMode(bool force=false); + virtual void toggleViewMode(bool force=false); - bool toggleVanityMode(bool enable); - void allowVanityMode(bool allow); + virtual bool toggleVanityMode(bool enable); + virtual void allowVanityMode(bool allow); /// @note this may be ignored if an important animation is currently playing void togglePreviewMode(bool enable); @@ -138,7 +142,7 @@ namespace MWRender bool isFirstPerson() const { return mFirstPersonView && mMode == Mode::Normal; } - void processViewChange(); + virtual void processViewChange(); void update(float duration, bool paused=false); @@ -149,11 +153,16 @@ namespace MWRender void setAnimation(NpcAnimation *anim); + osg::Camera* getOsgCamera(); + osg::Vec3d getFocalPoint() const; osg::Vec3d getFocalPointOffset() const; /// Stores focal and camera world positions in passed arguments - void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const; + virtual void getPosition(osg::Vec3d &focal, osg::Vec3d &camera) const; + + /// Store camera orientation in passed arguments + virtual void getOrientation(osg::Quat& orientation) const; bool isVanityOrPreviewModeEnabled() const { return mMode != Mode::Normal; } Mode getMode() const { return mMode; } diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index b538f0b7b..4e5458e41 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -959,7 +959,7 @@ void NpcAnimation::addControllers() mActiveControllers.emplace_back(node, mFirstPersonNeckController); } } - else if (mViewMode == VM_Normal) + else if (mViewMode != VM_HeadOnly) { WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); } diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 7e55001da..76cd4cc05 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -27,7 +27,10 @@ namespace MWRender class NeckController; class HeadAnimationTime; -class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener +class NpcAnimation : + public ActorAnimation + , public WeaponAnimation + , public MWWorld::InventoryStoreListener { public: void equipmentChanged() override; @@ -39,10 +42,12 @@ public: enum ViewMode { VM_Normal, VM_FirstPerson, - VM_HeadOnly + VM_HeadOnly, + VM_VRNormal, + VM_VRFirstPerson, }; -private: +protected: static const PartBoneMap sPartList; // Bounded Parts @@ -152,9 +157,9 @@ public: // WeaponAnimation void showWeapon(bool show) override { showWeapons(show); } - void setViewMode(ViewMode viewMode); + virtual void setViewMode(ViewMode viewMode); - void updateParts(); + virtual void updateParts(); /// Rebuilds the NPC, updating their root model, animation sources, and equipment. void rebuild(); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index e497fdecd..edd9547f3 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -31,6 +32,7 @@ #include #include +#include #include #include #include @@ -69,6 +71,15 @@ #include "screenshotmanager.hpp" #include "groundcover.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vranimation.hpp" +#include "../mwvr/vrpointer.hpp" +#include "../mwvr/vrviewer.hpp" +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vrcamera.hpp" +#endif + + namespace MWRender { @@ -184,7 +195,7 @@ namespace MWRender Resource::ResourceSystem* mResourceSystem; }; - RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, std::unique_ptr camera, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& resourcePath, DetourNavigator::Navigator& navigator) : mViewer(viewer) @@ -193,6 +204,9 @@ namespace MWRender , mWorkQueue(workQueue) , mUnrefQueue(new SceneUtil::UnrefQueue) , mNavigator(navigator) +#ifdef USE_OPENXR + , mUserPointer(new MWVR::UserPointer(rootNode)) +#endif , mMinimumAmbientLuminance(0.f) , mNightEyeFactor(0.f) , mFieldOfViewOverridden(false) @@ -207,7 +221,8 @@ namespace MWRender || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows") || lightingMethod != SceneUtil::LightingMethod::FFP; - resourceSystem->getSceneManager()->setForceShaders(forceShaders); + //resourceSystem->getSceneManager()->setForceShaders(forceShaders); + resourceSystem->getSceneManager()->setForceShaders(true); // FIXME: calling dummy method because terrain needs to know whether lighting is clamped resourceSystem->getSceneManager()->setClampLighting(Settings::Manager::getBool("clamp lighting", "Shaders")); resourceSystem->getSceneManager()->setAutoUseNormalMaps(Settings::Manager::getBool("auto use object normal maps", "Shaders")); @@ -356,8 +371,8 @@ namespace MWRender } // water goes after terrain for correct waterculling order mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath)); + mCamera = std::move(camera); - mCamera.reset(new Camera(mViewer->getCamera())); if (Settings::Manager::getBool("view over shoulder", "Camera")) mViewOverShoulderController.reset(new ViewOverShoulderController(mCamera.get())); @@ -411,7 +426,10 @@ namespace MWRender mViewer->getCamera()->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); mViewer->getCamera()->setCullingMode(cullingMode); - mViewer->getCamera()->setCullMask(~(Mask_UpdateVisitor|Mask_SimpleWater)); + auto mask = ~(Mask_UpdateVisitor | Mask_SimpleWater); + mViewer->getCamera()->setCullMask(mask); + mViewer->getCamera()->setCullMaskLeft(mask); + mViewer->getCamera()->setCullMaskRight(mask); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::NIFFile::setLoadUnsupportedFiles(Settings::Manager::getBool("load unsupported nif files", "Models")); @@ -424,6 +442,7 @@ namespace MWRender mFirstPersonFieldOfView = std::min(std::max(1.f, firstPersonFov), 179.f); mStateUpdater->setFogEnd(mViewDistance); + ////// Near far uniforms mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("near", mNearClip)); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("far", mViewDistance)); mRootNode->getOrCreateStateSet()->addUniform(new osg::Uniform("simpleWater", false)); @@ -435,6 +454,7 @@ namespace MWRender mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near"); mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far"); + updateProjectionMatrix(); } @@ -669,6 +689,8 @@ namespace MWRender else mask &= ~Mask_Scene; mViewer->getCamera()->setCullMask(mask); + mViewer->getCamera()->setCullMaskLeft(mask); + mViewer->getCamera()->setCullMaskRight(mask); return enabled; } else if (mode == Render_NavMesh) @@ -857,21 +879,26 @@ namespace MWRender return osg::Vec4f(min_x, min_y, max_x, max_y); } - RenderingManager::RayResult getIntersectionResult (osgUtil::LineSegmentIntersector* intersector) + RayResult getIntersectionResult (osgUtil::LineSegmentIntersector* intersector) { - RenderingManager::RayResult result; + RayResult result; result.mHit = false; result.mHitRefnum.unset(); result.mRatio = 0; + result.mHitNode = nullptr; if (intersector->containsIntersections()) { result.mHit = true; osgUtil::LineSegmentIntersector::Intersection intersection = intersector->getFirstIntersection(); + result.mHitPointLocal = intersection.getLocalIntersectPoint(); result.mHitPointWorld = intersection.getWorldIntersectPoint(); result.mHitNormalWorld = intersection.getWorldIntersectNormal(); result.mRatio = intersection.ratio; + if(!intersection.nodePath.empty()) + result.mHitNode = intersection.nodePath.back(); + PtrHolder* ptrHolder = nullptr; std::vector refnumMarkers; for (osg::NodePath::const_iterator it = intersection.nodePath.begin(); it != intersection.nodePath.end(); ++it) @@ -920,15 +947,15 @@ namespace MWRender unsigned int mask = ~0u; mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover); if (ignorePlayer) - mask &= ~(Mask_Player); + mask &= ~(Mask_Player|Mask_Pointer); if (ignoreActors) - mask &= ~(Mask_Actor|Mask_Player); + mask &= ~(Mask_Actor|Mask_Player|Mask_Pointer); mIntersectionVisitor->setTraversalMask(mask); return mIntersectionVisitor; } - RenderingManager::RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors) + RayResult RenderingManager::castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors) { osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL, origin, dest)); @@ -939,7 +966,25 @@ namespace MWRender return getIntersectionResult(intersector); } - RenderingManager::RayResult RenderingManager::castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors) + RayResult RenderingManager::castRay(const osg::Transform* source, float maxDistance, bool ignorePlayer, bool ignoreActors) + { + + if (source) + { + osg::Matrix worldMatrix = osg::computeLocalToWorld(source->getParentalNodePaths()[0]); + + osg::Vec3f direction = worldMatrix.getRotate() * osg::Vec3f(0, 1, 0); + direction.normalize(); + + osg::Vec3f raySource = worldMatrix.getTrans(); + osg::Vec3f rayTarget = worldMatrix.getTrans() + direction * maxDistance; + + return castRay(raySource, rayTarget, ignorePlayer, ignoreActors); + } + return RayResult(); + } + + RayResult RenderingManager::castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors) { osg::ref_ptr intersector (new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::PROJECTION, nX * 2.f - 1.f, nY * (-2.f) + 1.f)); @@ -982,6 +1027,10 @@ namespace MWRender notifyWorldSpaceChanged(); if (mObjectPaging) mObjectPaging->clear(); + +#ifdef USE_OPENXR + mUserPointer->setParent(nullptr); +#endif } MWRender::Animation* RenderingManager::getAnimation(const MWWorld::Ptr &ptr) @@ -1021,8 +1070,13 @@ namespace MWRender void RenderingManager::renderPlayer(const MWWorld::Ptr &player) { +#ifdef USE_OPENXR + MWVR::Environment::get().setPlayerAnimation(new MWVR::VRAnimation(player, player.getRefData().getBaseNode(), mResourceSystem, false, mUserPointer)); + mPlayerAnimation = MWVR::Environment::get().getPlayerAnimation(); +#else mPlayerAnimation = new NpcAnimation(player, player.getRefData().getBaseNode(), mResourceSystem, 0, NpcAnimation::VM_Normal, mFirstPersonFieldOfView); +#endif mCamera->setAnimation(mPlayerAnimation.get()); mCamera->attachTo(player); @@ -1114,6 +1168,12 @@ namespace MWRender void RenderingManager::setFogColor(const osg::Vec4f &color) { mViewer->getCamera()->setClearColor(color); + for (unsigned int i = 0; i < mViewer->getNumSlaves(); i++) + { + const auto& slave = mViewer->getSlave(i); + if (slave._camera) + slave._camera->setClearColor(color); + } mStateUpdater->setFogColor(color); } @@ -1350,4 +1410,11 @@ namespace MWRender if (mObjectPaging) mObjectPaging->getPagedRefnums(activeGrid, out); } + +#ifdef USE_OPENXR + MWVR::UserPointer& RenderingManager::userPointer() + { + return *mUserPointer; + } +#endif } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index a0a74bd5c..6f19e544f 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -68,6 +68,11 @@ namespace DetourNavigator struct Settings; } +namespace MWVR +{ + class UserPointer; +} + namespace MWRender { class GroundcoverUpdater; @@ -90,10 +95,25 @@ namespace MWRender class ObjectPaging; class Groundcover; + // Result data of ray cast methods. + // Needs to be declared outside the RenderingManager class to be forward declarable + struct RayResult + { + bool mHit; + osg::Vec3f mHitNormalWorld; + osg::Vec3f mHitPointWorld; + osg::Vec3f mHitPointLocal; + MWWorld::Ptr mHitObject; + osg::Node* mHitNode; + /// Cast a ray between two points + ESM::RefNum mHitRefnum; + float mRatio; + }; + class RenderingManager : public MWRender::RenderingInterface { public: - RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, std::unique_ptr camera, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const std::string& resourcePath, DetourNavigator::Navigator& navigator); ~RenderingManager(); @@ -110,6 +130,8 @@ namespace MWRender osg::Uniform* mUniformNear; osg::Uniform* mUniformFar; + osg::Uniform* mUniformStereoViewOffsets; + osg::Uniform* mUniformStereoProjections; void preloadCommonAssets(); @@ -153,18 +175,11 @@ namespace MWRender void screenshot(osg::Image* image, int w, int h); bool screenshot360(osg::Image* image); - struct RayResult - { - bool mHit; - osg::Vec3f mHitNormalWorld; - osg::Vec3f mHitPointWorld; - MWWorld::Ptr mHitObject; - ESM::RefNum mHitRefnum; - float mRatio; - }; - RayResult castRay(const osg::Vec3f& origin, const osg::Vec3f& dest, bool ignorePlayer, bool ignoreActors=false); + /// Cast a ray from a node in the scene graph + RayResult castRay(const osg::Transform* source, float maxDistance, bool ignorePlayer, bool ignoreActors=false); + /// Return the object under the mouse cursor / crosshair position, given by nX and nY normalized screen coordinates, /// where (0,0) is the top left corner. RayResult castCameraToViewportRay(const float nX, const float nY, float maxDistance, bool ignorePlayer, bool ignoreActors=false); @@ -240,6 +255,10 @@ namespace MWRender bool pagingUnlockCache(); void getPagedRefnums(const osg::Vec4i &activeGrid, std::set &out); +#ifdef USE_OPENXR + MWVR::UserPointer& userPointer(); +#endif + private: void updateProjectionMatrix(); void updateTextureFiltering(); @@ -293,6 +312,10 @@ namespace MWRender std::unique_ptr mViewOverShoulderController; osg::Vec3f mCurrentCameraPos; +#ifdef USE_OPENXR + std::shared_ptr mUserPointer; +#endif + osg::ref_ptr mStateUpdater; osg::Vec4f mAmbientColor; diff --git a/apps/openmw/mwrender/screenshotmanager.cpp b/apps/openmw/mwrender/screenshotmanager.cpp index f474a5a9f..b769943b6 100644 --- a/apps/openmw/mwrender/screenshotmanager.cpp +++ b/apps/openmw/mwrender/screenshotmanager.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -113,7 +114,7 @@ namespace MWRender void ScreenshotManager::screenshot(osg::Image* image, int w, int h) { - osg::Camera* camera = mViewer->getCamera(); + osg::Camera * camera = mViewer->getCamera(); osg::ref_ptr tempDrw = new osg::Drawable; tempDrw->setDrawCallback(new ReadImageFromFramebufferCallback(image, w, h)); tempDrw->setCullingActive(false); @@ -324,8 +325,8 @@ namespace MWRender rttCamera->setUpdateCallback(new NoTraverseCallback); rttCamera->addChild(mSceneRoot); - rttCamera->addChild(mWater->getReflectionCamera()); - rttCamera->addChild(mWater->getRefractionCamera()); + rttCamera->addChild(mWater->getReflectionNode()); + rttCamera->addChild(mWater->getRefractionNode()); rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); diff --git a/apps/openmw/mwrender/util.cpp b/apps/openmw/mwrender/util.cpp index e3fc48040..a224f47e3 100644 --- a/apps/openmw/mwrender/util.cpp +++ b/apps/openmw/mwrender/util.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -64,4 +65,20 @@ void overrideTexture(const std::string &texture, Resource::ResourceSystem *resou node->setStateSet(stateset); } +MipmapCallback::~MipmapCallback() +{ + +} + +void MipmapCallback::operator()(osg::RenderInfo& renderInfo) const +{ + auto* gl = renderInfo.getState()->get(); + auto* tex = mTexture->getTextureObject(renderInfo.getContextID()); + if (tex) + { + tex->bind(); + gl->glGenerateMipmap(tex->target()); + } +} + } diff --git a/apps/openmw/mwrender/util.hpp b/apps/openmw/mwrender/util.hpp index a89baa22b..6847b08fe 100644 --- a/apps/openmw/mwrender/util.hpp +++ b/apps/openmw/mwrender/util.hpp @@ -2,12 +2,14 @@ #define OPENMW_MWRENDER_UTIL_H #include +#include #include #include namespace osg { class Node; + class Texture2D; } namespace Resource @@ -32,6 +34,24 @@ namespace MWRender // no traverse() } }; + + /// Draw callback for RTT that can be used to regenerate mipmaps + /// either as a predraw before use or a postdraw after RTT. + class MipmapCallback : public osg::Camera::DrawCallback + { + public: + MipmapCallback(osg::Texture2D* texture) + : mTexture(texture) + {} + + ~MipmapCallback(); + + void operator()(osg::RenderInfo& info) const override; + + private: + + osg::ref_ptr mTexture; + }; } #endif diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 87ca9415f..80bf6d6cf 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -56,6 +56,10 @@ namespace MWRender Mask_Lighting = (1<<19), Mask_Groundcover = (1<<20), + + // Vr masks + Mask_3DGUI = (1 << 21), + Mask_Pointer = (1 << 22) }; } diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index c5fd1a363..1fd64bb27 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -25,12 +26,14 @@ #include #include +#include #include #include #include #include #include +#include #include @@ -42,6 +45,8 @@ #include "../mwworld/cellstore.hpp" +#include "../mwbase/environment.hpp" + #include "vismask.hpp" #include "ripplesimulation.hpp" #include "renderbin.hpp" @@ -261,65 +266,46 @@ osg::ref_ptr readPngImage (const std::string& file) return result.getImage(); } - -class Refraction : public osg::Camera +class Refraction : public SceneUtil::RTTNode { public: - Refraction() + Refraction(uint32_t rttSize) + : RTTNode(rttSize, rttSize, 1, false) { - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setRenderOrder(osg::Camera::PRE_RENDER, 1); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - osg::Camera::setName("RefractionCamera"); - setCullCallback(new InheritViewPointCallback); - setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); - - setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting|Mask_Groundcover); - setNodeMask(Mask_RenderToTexture); - setViewport(0, 0, rttSize, rttSize); - - // No need for Update traversal since the scene 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 NoTraverseCallback); + mClipCullNode = new ClipCullNode; + } + + void setDefaults(osg::Camera* camera) override + { + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); + camera->setName("RefractionCamera"); + camera->addCullCallback(new InheritViewPointCallback); + camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR); + + camera->setCullMask(Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting); // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog // assign large value to effectively turn off fog // shaders don't respect glDisable(GL_FOG) - osg::ref_ptr fog (new osg::Fog); + osg::ref_ptr fog(new osg::Fog); fog->setStart(10000000); fog->setEnd(10000000); - getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + camera->getOrCreateStateSet()->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); - mClipCullNode = new ClipCullNode; - osg::Camera::addChild(mClipCullNode); - - mRefractionTexture = new osg::Texture2D; - mRefractionTexture->setTextureSize(rttSize, rttSize); - mRefractionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionTexture->setInternalFormat(GL_RGB); - mRefractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mRefractionTexture); - - mRefractionDepthTexture = new osg::Texture2D; - mRefractionDepthTexture->setTextureSize(rttSize, rttSize); - mRefractionDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); - mRefractionDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); - mRefractionDepthTexture->setSourceType(GL_UNSIGNED_INT); - mRefractionDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mRefractionDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - - attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); if (Settings::Manager::getFloat("refraction scale", "Water") != 1) // TODO: to be removed with issue #5709 - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); + + if (MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager().stereoGeometryShaderEnabled()) + Misc::enableStereoForCamera(camera, true); + } + + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); } void setScene(osg::Node* scene) @@ -332,74 +318,57 @@ public: void setWaterLevel(float waterLevel) { - const float refractionScale = std::min(1.0f,std::max(0.0f, + const float refractionScale = std::min(1.0f, std::max(0.0f, Settings::Manager::getFloat("refraction scale", "Water"))); - setViewMatrix(osg::Matrix::scale(1,1,refractionScale) * - osg::Matrix::translate(0,0,(1.0 - refractionScale) * waterLevel)); - - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); - } - - osg::Texture2D* getRefractionTexture() const - { - return mRefractionTexture.get(); - } + mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * + osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); - osg::Texture2D* getRefractionDepthTexture() const - { - return mRefractionDepthTexture.get(); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } private: osg::ref_ptr mClipCullNode; - osg::ref_ptr mRefractionTexture; - osg::ref_ptr mRefractionDepthTexture; osg::ref_ptr mScene; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; }; -class Reflection : public osg::Camera +class Reflection : public SceneUtil::RTTNode { public: - Reflection(bool isInterior) + Reflection(uint32_t rttSize, bool isInterior) + : RTTNode(rttSize, rttSize, 0, false) { - setRenderOrder(osg::Camera::PRE_RENDER); - setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); - setReferenceFrame(osg::Camera::RELATIVE_RF); - setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); - osg::Camera::setName("ReflectionCamera"); - setCullCallback(new InheritViewPointCallback); - setInterior(isInterior); - setNodeMask(Mask_RenderToTexture); - - unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); - setViewport(0, 0, rttSize, rttSize); - - // 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 NoTraverseCallback); - - mReflectionTexture = new osg::Texture2D; - mReflectionTexture->setTextureSize(rttSize, rttSize); - mReflectionTexture->setInternalFormat(GL_RGB); - mReflectionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); - mReflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); - mReflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + mClipCullNode = new ClipCullNode; + } - SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(this, osg::Camera::COLOR_BUFFER, mReflectionTexture); + void setDefaults(osg::Camera* camera) override + { + camera->setReferenceFrame(osg::Camera::RELATIVE_RF); + camera->setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); + camera->setName("ReflectionCamera"); + camera->addCullCallback(new InheritViewPointCallback); + // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. - osg::ref_ptr frontFace (new osg::FrontFace); + osg::ref_ptr frontFace(new osg::FrontFace); frontFace->setMode(osg::FrontFace::CLOCKWISE); - getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); + camera->getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); - mClipCullNode = new ClipCullNode; - osg::Camera::addChild(mClipCullNode); + camera->addChild(mClipCullNode); + camera->setNodeMask(Mask_RenderToTexture); + + SceneUtil::ShadowManager::disableShadowsForStateSet(camera->getOrCreateStateSet()); - SceneUtil::ShadowManager::disableShadowsForStateSet(getOrCreateStateSet()); + if (MWBase::Environment::get().getResourceSystem()->getSceneManager()->getShaderManager().stereoGeometryShaderEnabled()) + Misc::enableStereoForCamera(camera, true); + } + + void apply(osg::Camera* camera) override + { + camera->setViewMatrix(mViewMatrix); + camera->setCullMask(mNodeMask); } void setInterior(bool isInterior) @@ -409,16 +378,16 @@ public: unsigned int extraMask = 0; if(reflectionDetail >= 1) extraMask |= Mask_Terrain; if(reflectionDetail >= 2) extraMask |= Mask_Static; - if(reflectionDetail >= 3) extraMask |= Mask_Effect|Mask_ParticleSystem|Mask_Object; - if(reflectionDetail >= 4) extraMask |= Mask_Player|Mask_Actor; + if(reflectionDetail >= 3) extraMask |= Mask_Effect | Mask_ParticleSystem | Mask_Object; + if(reflectionDetail >= 4) extraMask |= Mask_Player | Mask_Actor; if(reflectionDetail >= 5) extraMask |= Mask_Groundcover; - setCullMask(Mask_Scene|Mask_Sky|Mask_Lighting|extraMask); + mNodeMask = Mask_Scene | Mask_Sky | Mask_Lighting | extraMask; } void setWaterLevel(float waterLevel) { - setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); - mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0,0,1), osg::Vec3d(0,0,waterLevel))); + mViewMatrix = osg::Matrix::scale(1, 1, -1) * osg::Matrix::translate(0, 0, 2 * waterLevel); + mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, 1), osg::Vec3d(0, 0, waterLevel))); } void setScene(osg::Node* scene) @@ -429,15 +398,11 @@ public: mClipCullNode->addChild(scene); } - osg::Texture2D* getReflectionTexture() const - { - return mReflectionTexture.get(); - } - private: - osg::ref_ptr mReflectionTexture; osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; + osg::Node::NodeMask mNodeMask; + osg::Matrix mViewMatrix{ osg::Matrix::identity() }; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -474,6 +439,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem , mTop(0) , mInterior(false) , mCullCallback(nullptr) + , mShaderWaterStateSetUpdater(nullptr) { mSimulation.reset(new RippleSimulation(mSceneRoot, resourceSystem)); @@ -528,22 +494,31 @@ void Water::setCullCallback(osg::Callback* callback) void Water::updateWaterMaterial() { + if (mShaderWaterStateSetUpdater) + { + mWaterNode->removeCullCallback(mShaderWaterStateSetUpdater); + mShaderWaterStateSetUpdater = nullptr; + } if (mReflection) { - mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } + mWaterNode->setStateSet(nullptr); + mWaterGeom->setStateSet(nullptr); + mWaterGeom->setUpdateCallback(nullptr); + if (Settings::Manager::getBool("shader", "Water")) { - mReflection = new Reflection(mInterior); + unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); + + mReflection = new Reflection(rttSize, mInterior); mReflection->setWaterLevel(mTop); mReflection->setScene(mSceneRoot); if (mCullCallback) @@ -552,7 +527,7 @@ void Water::updateWaterMaterial() if (Settings::Manager::getBool("refraction", "Water")) { - mRefraction = new Refraction; + mRefraction = new Refraction(rttSize); mRefraction->setWaterLevel(mTop); mRefraction->setScene(mSceneRoot); if (mCullCallback) @@ -560,7 +535,7 @@ void Water::updateWaterMaterial() mParent->addChild(mRefraction); } - createShaderWaterStateSet(mWaterGeom, mReflection, mRefraction); + createShaderWaterStateSet(mWaterNode, mReflection, mRefraction); } else createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); @@ -568,12 +543,12 @@ void Water::updateWaterMaterial() updateVisible(); } -osg::Camera *Water::getReflectionCamera() +osg::Node *Water::getReflectionNode() { return mReflection; } -osg::Camera *Water::getRefractionCamera() +osg::Node* Water::getRefractionNode() { return mRefraction; } @@ -620,17 +595,79 @@ void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) sceneManager->setForceShaders(oldValue); } +class ShaderWaterStateSetUpdater : public SceneUtil::StateSetUpdater +{ +public: + ShaderWaterStateSetUpdater(Water* water, Reflection* reflection, Refraction* refraction, osg::ref_ptr program, osg::ref_ptr normalMap) + : mWater(water) + , mReflection(reflection) + , mRefraction(refraction) + , mProgram(program) + , mNormalMap(normalMap) + { + } + + void setDefaults(osg::StateSet* stateset) override + { + stateset->addUniform(new osg::Uniform("normalMap", 0)); + stateset->setTextureAttributeAndModes(0, mNormalMap, osg::StateAttribute::ON); + stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + stateset->setAttributeAndModes(mProgram, osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("reflectionMap", 1)); + if (mRefraction) + { + stateset->addUniform(new osg::Uniform("refractionMap", 2)); + stateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); + stateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); + } + else + { + stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); + osg::ref_ptr depth(new osg::Depth); + depth->setWriteMask(false); + stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + } + } + + void apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + stateset->setTextureAttributeAndModes(1, mReflection->getColorTexture(cv), osg::StateAttribute::ON); + + if (mRefraction) + { + stateset->setTextureAttributeAndModes(2, mRefraction->getColorTexture(cv), osg::StateAttribute::ON); + stateset->setTextureAttributeAndModes(3, mRefraction->getDepthTexture(cv), osg::StateAttribute::ON); + } + } + +private: + Water* mWater; + Reflection* mReflection; + Refraction* mRefraction; + osg::ref_ptr mProgram; + osg::ref_ptr mNormalMap; +}; + void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, Refraction* refraction) { // use a define map to conditionally compile the shader std::map defineMap; - defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(refraction ? "1" : "0"))); + defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(mRefraction ? "1" : "0"))); + + if (mResourceSystem->getSceneManager()->getShaderManager().stereoGeometryShaderEnabled()) + { + defineMap["geometryShader"] = "1"; + } Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); - osg::ref_ptr vertexShader (shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); - osg::ref_ptr fragmentShader (shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr vertexShader(shaderMgr.getShader("water_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr fragmentShader(shaderMgr.getShader("water_fragment.glsl", defineMap, osg::Shader::FRAGMENT)); + osg::ref_ptr program = shaderMgr.getProgram(vertexShader, fragmentShader); - osg::ref_ptr normalMap (new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); + osg::ref_ptr normalMap(new osg::Texture2D(readPngImage(mResourcePath + "/shaders/water_nm.png"))); if (normalMap->getImage()) normalMap->getImage()->flipVertical(); @@ -639,47 +676,16 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R normalMap->setMaxAnisotropy(16); normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + - osg::ref_ptr shaderStateset = new osg::StateSet; - shaderStateset->addUniform(new osg::Uniform("normalMap", 0)); - shaderStateset->addUniform(new osg::Uniform("reflectionMap", 1)); - - shaderStateset->setTextureAttributeAndModes(0, normalMap, osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(1, reflection->getReflectionTexture(), osg::StateAttribute::ON); - - if (refraction) - { - shaderStateset->setTextureAttributeAndModes(2, refraction->getRefractionTexture(), osg::StateAttribute::ON); - shaderStateset->setTextureAttributeAndModes(3, refraction->getRefractionDepthTexture(), osg::StateAttribute::ON); - shaderStateset->addUniform(new osg::Uniform("refractionMap", 2)); - shaderStateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); - } - else - { - shaderStateset->setMode(GL_BLEND, osg::StateAttribute::ON); - - shaderStateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); - - osg::ref_ptr depth (new osg::Depth); - depth->setWriteMask(false); - shaderStateset->setAttributeAndModes(depth, osg::StateAttribute::ON); - } - - shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); - - osg::ref_ptr program (new osg::Program); - program->addShader(vertexShader); - program->addShader(fragmentShader); auto method = mResourceSystem->getSceneManager()->getLightingMethod(); if (method == SceneUtil::LightingMethod::SingleUBO) program->addBindUniformBlock("LightBufferBinding", static_cast(Shader::UBOBinding::LightBuffer)); - shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); - - node->setStateSet(shaderStateset); - mRainIntensityUpdater = new RainIntensityUpdater(); node->setUpdateCallback(mRainIntensityUpdater); + + mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater(this, mReflection, mRefraction, program, normalMap); + node->addCullCallback(mShaderWaterStateSetUpdater); } void Water::processChangedSettings(const Settings::CategorySettingVector& settings) @@ -693,13 +699,11 @@ Water::~Water() if (mReflection) { - mReflection->removeChildren(0, mReflection->getNumChildren()); mParent->removeChild(mReflection); mReflection = nullptr; } if (mRefraction) { - mRefraction->removeChildren(0, mRefraction->getNumChildren()); mParent->removeChild(mRefraction); mRefraction = nullptr; } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index ec7dc95db..8b6c5c83e 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -72,6 +72,7 @@ namespace MWRender bool mInterior; osg::Callback* mCullCallback; + osg::ref_ptr mShaderWaterStateSetUpdater; osg::Vec3f getSceneNodeCoordinates(int gridX, int gridY); void updateVisible(); @@ -116,8 +117,8 @@ namespace MWRender void update(float dt); - osg::Camera *getReflectionCamera(); - osg::Camera *getRefractionCamera(); + osg::Node* getReflectionNode(); + osg::Node* getRefractionNode(); void processChangedSettings(const Settings::CategorySettingVector& settings); }; diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 11eb60f88..d67c0a2f0 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -26,6 +26,12 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/weapontype.hpp" +#include "../mwmechanics/actorutil.hpp" + +#ifdef USE_OPENXR +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vranimation.hpp" +#endif #include "animation.hpp" #include "rotatecontroller.hpp" @@ -154,8 +160,15 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) */ // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's orientation dictates otherwise. - osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); + osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1, 0, 0)) + * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0, 0, -1)); + +#ifdef USE_OPENXR + bool isPlayer = actor == MWMechanics::getPlayer(); + // In VR weapon aim is taken from the real orientation of the weapon. + if(isPlayer) + orient = MWVR::Environment::get().getPlayerAnimation()->getWeaponTransformMatrix().getRotate(); +#endif const MWWorld::Store &gmst = MWBase::Environment::get().getWorld()->getStore().get(); @@ -228,6 +241,9 @@ void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) MWWorld::Ptr weaponPtr = *weapon; MWWorld::Ptr ammoPtr = *ammo; +#ifdef USE_OPENXR + orient = osg::computeLocalToWorld(nodepaths[0]).getRotate(); +#endif MWBase::Environment::get().getWorld()->launchProjectile(actor, ammoPtr, launchPos, orient, weaponPtr, speed, attackStrength); inv.remove(ammoPtr, 1, actor); diff --git a/apps/openmw/mwvr/openxraction.cpp b/apps/openmw/mwvr/openxraction.cpp new file mode 100644 index 000000000..eb5196a3c --- /dev/null +++ b/apps/openmw/mwvr/openxraction.cpp @@ -0,0 +1,89 @@ +#include "openxraction.hpp" +#include "openxrdebug.hpp" +#include "vrenvironment.hpp" +#include "openxrmanagerimpl.hpp" + +namespace MWVR +{ + + OpenXRAction::OpenXRAction( + XrAction action, + XrActionType actionType, + const std::string& actionName, + const std::string& localName) + : mAction(action) + , mType(actionType) + , mName(actionName) + , mLocalName(localName) + { + VrDebug::setName(action, "OpenMW XR Action " + actionName); + }; + + 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; + } +} diff --git a/apps/openmw/mwvr/openxraction.hpp b/apps/openmw/mwvr/openxraction.hpp new file mode 100644 index 000000000..dd2792049 --- /dev/null +++ b/apps/openmw/mwvr/openxraction.hpp @@ -0,0 +1,36 @@ +#ifndef OPENXR_ACTION_HPP +#define OPENXR_ACTION_HPP + +#include + +#include + + +namespace MWVR +{ + /// \brief C++ wrapper for the XrAction type + struct OpenXRAction + { + private: + OpenXRAction(const OpenXRAction&) = default; + OpenXRAction& operator=(const OpenXRAction&) = default; + public: + OpenXRAction(XrAction action, XrActionType actionType, const std::string& actionName, const std::string& localName); + ~OpenXRAction(); + + //! Convenience + operator XrAction() { return mAction; } + + bool getFloat(XrPath subactionPath, float& value); + bool getBool(XrPath subactionPath, bool& value); + bool getPoseIsActive(XrPath subactionPath); + bool applyHaptics(XrPath subactionPath, float amplitude); + + XrAction mAction = XR_NULL_HANDLE; + XrActionType mType; + std::string mName; + std::string mLocalName; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxractionset.cpp b/apps/openmw/mwvr/openxractionset.cpp new file mode 100644 index 000000000..2b63ab8b1 --- /dev/null +++ b/apps/openmw/mwvr/openxractionset.cpp @@ -0,0 +1,232 @@ +#include "openxractionset.hpp" +#include "openxrdebug.hpp" + +#include "vrenvironment.hpp" +#include "openxrmanager.hpp" +#include "openxrmanagerimpl.hpp" +#include "openxraction.hpp" + +#include + +#include + +#include + +// TODO: should implement actual safe strcpy +#ifdef __linux__ +#define strcpy_s(dst, src) int(strcpy(dst, src) != nullptr) +#endif + +namespace MWVR +{ + + OpenXRActionSet::OpenXRActionSet(const std::string& actionSetName, std::shared_ptr deadzone) + : mActionSet(nullptr) + , mLocalizedName(actionSetName) + , mInternalName(Misc::StringUtils::lowerCase(actionSetName)) + , mDeadzone(deadzone) + { + mActionSet = createActionSet(actionSetName); + // When starting to account for more devices than oculus touch, this section may need some expansion/redesign. + }; + + void + OpenXRActionSet::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 + OpenXRActionSet::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 + OpenXRActionSet::createMWAction( + int openMWAction, + const std::string& actionName, + const std::string& localName) + { + auto xrAction = createXRAction(AxisAction::ActionType, mInternalName + "_" + actionName, mLocalizedName + " " + localName); + mActionMap.emplace(actionName, new AxisAction(openMWAction, std::move(xrAction), mDeadzone)); + } + + template + void + OpenXRActionSet::createMWAction( + int openMWAction, + const std::string& actionName, + const std::string& localName) + { + auto xrAction = createXRAction(A::ActionType, mInternalName + "_" + actionName, mLocalizedName + " " + localName); + mActionMap.emplace(actionName, new A(openMWAction, std::move(xrAction))); + } + + + void + OpenXRActionSet::createMWAction( + VrControlType controlType, + int openMWAction, + const std::string& actionName, + const std::string& localName) + { + switch (controlType) + { + case VrControlType::Press: + return createMWAction(openMWAction, actionName, localName); + case VrControlType::LongPress: + return createMWAction(openMWAction, actionName, localName); + case VrControlType::Hold: + return createMWAction(openMWAction, actionName, localName); + case VrControlType::Axis: + return createMWAction(openMWAction, actionName, localName); + //case VrControlType::Pose: + // return createMWAction(openMWAction, actionName, localName); + //case VrControlType::Haptic: + // return createMWAction(openMWAction, actionName, localName); + default: + Log(Debug::Warning) << "createMWAction: pose/haptics Not implemented here"; + } + } + + + XrActionSet + OpenXRActionSet::createActionSet(const std::string& name) + { + std::string localized_name = name; + std::string internal_name = Misc::StringUtils::lowerCase(name); + auto* xr = Environment::get().getManager(); + XrActionSet actionSet = XR_NULL_HANDLE; + XrActionSetCreateInfo createInfo{ XR_TYPE_ACTION_SET_CREATE_INFO }; + strcpy_s(createInfo.actionSetName, internal_name.c_str()); + strcpy_s(createInfo.localizedActionSetName, localized_name.c_str()); + createInfo.priority = 0; + CHECK_XRCMD(xrCreateActionSet(xr->impl().xrInstance(), &createInfo, &actionSet)); + VrDebug::setName(actionSet, "OpenMW XR Action Set " + name); + return actionSet; + } + + void OpenXRActionSet::suggestBindings(std::vector& xrSuggestedBindings, const SuggestedBindings& mwSuggestedBindings) + { + std::vector suggestedBindings; + if (!mTrackerMap.empty()) + { + suggestedBindings.emplace_back(XrActionSuggestedBinding{ *mTrackerMap[TrackedLimb::LEFT_HAND], getXrPath("/user/hand/left/input/aim/pose") }); + suggestedBindings.emplace_back(XrActionSuggestedBinding{ *mTrackerMap[TrackedLimb::RIGHT_HAND], getXrPath("/user/hand/right/input/aim/pose") }); + } + if(!mHapticsMap.empty()) + { + suggestedBindings.emplace_back(XrActionSuggestedBinding{ *mHapticsMap[TrackedLimb::LEFT_HAND], getXrPath("/user/hand/left/output/haptic") }); + suggestedBindings.emplace_back(XrActionSuggestedBinding{ *mHapticsMap[TrackedLimb::RIGHT_HAND], getXrPath("/user/hand/right/output/haptic") }); + }; + + for (auto& mwSuggestedBinding : mwSuggestedBindings) + { + auto xrAction = mActionMap.find(mwSuggestedBinding.action); + if (xrAction == mActionMap.end()) + { + Log(Debug::Error) << "OpenXRActionSet: Unknown action " << mwSuggestedBinding.action; + continue; + } + suggestedBindings.push_back({ *xrAction->second, getXrPath(mwSuggestedBinding.path) }); + } + + xrSuggestedBindings.insert(xrSuggestedBindings.end(), suggestedBindings.begin(), suggestedBindings.end()); + } + + XrSpace OpenXRActionSet::xrActionSpace(TrackedLimb limb) + { + return mTrackerMap[limb]->xrSpace(); + } + + + std::unique_ptr + OpenXRActionSet::createXRAction( + XrActionType actionType, + const std::string& actionName, + const std::string& localName) + { + 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 + OpenXRActionSet::updateControls() + { + auto* xr = Environment::get().getManager(); + if (!xr->impl().appShouldReadInput()) + 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)); + + mActionQueue.clear(); + for (auto& action : mActionMap) + action.second->updateAndQueue(mActionQueue); + } + + XrPath OpenXRActionSet::getXrPath(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* OpenXRActionSet::nextAction() + { + if (mActionQueue.empty()) + return nullptr; + + const auto* action = mActionQueue.front(); + mActionQueue.pop_front(); + return action; + + } + + Pose + OpenXRActionSet::getLimbPose( + int64_t time, + TrackedLimb limb) + { + auto it = mTrackerMap.find(limb); + if (it == mTrackerMap.end()) + { + Log(Debug::Error) << "OpenXRActionSet: No such tracker: " << limb; + return Pose{}; + } + + it->second->update(time); + return it->second->value(); + } + + void OpenXRActionSet::applyHaptics(TrackedLimb limb, float intensity) + { + auto it = mHapticsMap.find(limb); + if (it == mHapticsMap.end()) + { + Log(Debug::Error) << "OpenXRActionSet: No such tracker: " << limb; + return; + } + + it->second->apply(intensity); + } +} diff --git a/apps/openmw/mwvr/openxractionset.hpp b/apps/openmw/mwvr/openxractionset.hpp new file mode 100644 index 000000000..b2e573527 --- /dev/null +++ b/apps/openmw/mwvr/openxractionset.hpp @@ -0,0 +1,58 @@ +#ifndef OPENXR_ACTIONSET_HPP +#define OPENXR_ACTIONSET_HPP + +#include "vrinput.hpp" + +#include +#include + +namespace MWVR +{ + /// \brief Generates and manages an OpenXR ActionSet and associated actions. + class OpenXRActionSet + { + public: + using Actions = MWInput::Actions; + + OpenXRActionSet(const std::string& actionSetName, std::shared_ptr deadzone); + + //! 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); + + XrActionSet xrActionSet() { return mActionSet; }; + void suggestBindings(std::vector& xrSuggestedBindings, const SuggestedBindings& mwSuggestedBindings); + + XrSpace xrActionSpace(TrackedLimb limb); + + void createMWAction(VrControlType controlType, 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); + + protected: + template + void createMWAction(int openMWAction, const std::string& actionName, const std::string& localName); + std::unique_ptr createXRAction(XrActionType actionType, const std::string& actionName, const std::string& localName); + XrPath getXrPath(const std::string& path); + XrActionSet createActionSet(const std::string& name); + + XrActionSet mActionSet{ nullptr }; + std::string mLocalizedName{}; + std::string mInternalName{}; + std::map> mActionMap; + std::map> mTrackerMap; + std::map> mHapticsMap; + std::deque mActionQueue{}; + std::shared_ptr mDeadzone; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrdebug.cpp b/apps/openmw/mwvr/openxrdebug.cpp new file mode 100644 index 000000000..0f3437cc1 --- /dev/null +++ b/apps/openmw/mwvr/openxrdebug.cpp @@ -0,0 +1,36 @@ +#include "openxrdebug.hpp" +#include "openxrmanagerimpl.hpp" +#include "vrenvironment.hpp" + +// The OpenXR SDK's platform headers assume we've included these windows headers +#ifdef _WIN32 +#include +#include + +#elif __linux__ +#include +#include +#undef None + +#else +#error Unsupported platform +#endif + +#include +#include + +void MWVR::VrDebug::setName(uint64_t handle, XrObjectType type, const std::string& name) +{ + auto& xrManager = Environment::get().getManager()->impl(); + if (xrManager.xrExtensionIsEnabled(XR_EXT_DEBUG_UTILS_EXTENSION_NAME)) + { + XrDebugUtilsObjectNameInfoEXT nameInfo{ XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT, nullptr }; + nameInfo.objectHandle = handle; + nameInfo.objectType = type; + nameInfo.objectName = name.c_str(); + + static PFN_xrSetDebugUtilsObjectNameEXT setDebugUtilsObjectNameEXT + = reinterpret_cast(xrManager.xrGetFunction("xrSetDebugUtilsObjectNameEXT")); + CHECK_XRCMD(setDebugUtilsObjectNameEXT(xrManager.xrInstance(), &nameInfo)); + } +} diff --git a/apps/openmw/mwvr/openxrdebug.hpp b/apps/openmw/mwvr/openxrdebug.hpp new file mode 100644 index 000000000..c0dbba44a --- /dev/null +++ b/apps/openmw/mwvr/openxrdebug.hpp @@ -0,0 +1,73 @@ +#ifndef OPENXR_DEBUG_HPP +#define OPENXR_DEBUG_HPP + +#include + +#include + +namespace MWVR +{ + namespace VrDebug + { + //! Translates an OpenXR object to the associated XrObjectType enum value + template XrObjectType getObjectType(T t); + + //! Associates a name with an OpenXR symbol if XR_EXT_debug_utils is enabled + template void setName(T t, const std::string& name); + + //! Associates a name with an OpenXR symbol if XR_EXT_debug_utils is enabled + void setName(uint64_t handle, XrObjectType type, const std::string& name); + } +} + +template inline void MWVR::VrDebug::setName(T t, const std::string& name) +{ + setName(reinterpret_cast(t), getObjectType(t), name); +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrInstance) +{ + return XR_OBJECT_TYPE_INSTANCE; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrSession) +{ + return XR_OBJECT_TYPE_SESSION; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrSpace) +{ + return XR_OBJECT_TYPE_SPACE; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrActionSet) +{ + return XR_OBJECT_TYPE_ACTION_SET; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrAction) +{ + return XR_OBJECT_TYPE_ACTION; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrDebugUtilsMessengerEXT) +{ + return XR_OBJECT_TYPE_DEBUG_UTILS_MESSENGER_EXT; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrSpatialAnchorMSFT) +{ + return XR_OBJECT_TYPE_SPATIAL_ANCHOR_MSFT; +} + +template<> inline XrObjectType MWVR::VrDebug::getObjectType(XrHandTrackerEXT) +{ + return XR_OBJECT_TYPE_HAND_TRACKER_EXT; +} + +template inline XrObjectType MWVR::VrDebug::getObjectType(T t) +{ + return XR_OBJECT_TYPE_UNKNOWN; +} + +#endif diff --git a/apps/openmw/mwvr/openxrinput.cpp b/apps/openmw/mwvr/openxrinput.cpp new file mode 100644 index 000000000..3a091a93c --- /dev/null +++ b/apps/openmw/mwvr/openxrinput.cpp @@ -0,0 +1,188 @@ +#include "openxrinput.hpp" + +#include "vrenvironment.hpp" +#include "openxrmanager.hpp" +#include "openxrmanagerimpl.hpp" +#include "openxraction.hpp" + +#include + +#include + +#include + +namespace MWVR +{ + + OpenXRInput::OpenXRInput(std::shared_ptr deadzone) + { + mActionSets.emplace(ActionSet::Gameplay, OpenXRActionSet("Gameplay", deadzone)); + mActionSets.emplace(ActionSet::GUI, OpenXRActionSet("GUI", deadzone)); + mActionSets.emplace(ActionSet::Tracking, OpenXRActionSet("Tracking", deadzone)); + mActionSets.emplace(ActionSet::Haptics, OpenXRActionSet("Haptics", deadzone)); + + + /* + // 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 + */ + + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_GameMenu, "game_menu", "Game Menu"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, A_VrMetaMenu, "meta_menu", "Meta Menu"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::LongPress, A_Recenter, "reposition_menu", "Reposition Menu"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_Inventory, "inventory", "Inventory"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_Activate, "activate", "Activate"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Hold, MWInput::A_Use, "use", "Use"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Hold, MWInput::A_Jump, "jump", "Jump"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_ToggleWeapon, "weapon", "Weapon"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_ToggleSpell, "spell", "Spell"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_CycleSpellLeft, "cycle_spell_left", "Cycle Spell Left"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_CycleSpellRight, "cycle_spell_right", "Cycle Spell Right"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_CycleWeaponLeft, "cycle_weapon_left", "Cycle Weapon Left"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_CycleWeaponRight, "cycle_weapon_right", "Cycle Weapon Right"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Hold, MWInput::A_Sneak, "sneak", "Sneak"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_QuickKeysMenu, "quick_menu", "Quick Menu"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Axis, MWInput::A_LookLeftRight, "look_left_right", "Look Left Right"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Axis, MWInput::A_MoveForwardBackward, "move_forward_backward", "Move Forward Backward"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Axis, MWInput::A_MoveLeftRight, "move_left_right", "Move Left Right"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_Journal, "journal_book", "Journal Book"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_QuickSave, "quick_save", "Quick Save"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_Rest, "rest", "Rest"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Axis, A_ActivateTouch, "activate_touched", "Activate Touch"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_AlwaysRun, "always_run", "Always Run"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_AutoMove, "auto_move", "Auto Move"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_ToggleHUD, "toggle_hud", "Toggle HUD"); + getActionSet(ActionSet::Gameplay).createMWAction(VrControlType::Press, MWInput::A_ToggleDebug, "toggle_debug", "Toggle the debug hud"); + + getActionSet(ActionSet::GUI).createMWAction(VrControlType::Press, MWInput::A_GameMenu, "game_menu", "Game Menu"); + getActionSet(ActionSet::GUI).createMWAction(VrControlType::LongPress, A_Recenter, "reposition_menu", "Reposition Menu"); + getActionSet(ActionSet::GUI).createMWAction(VrControlType::Axis, A_MenuUpDown, "menu_up_down", "Menu Up Down"); + getActionSet(ActionSet::GUI).createMWAction(VrControlType::Axis, A_MenuLeftRight, "menu_left_right", "Menu Left Right"); + getActionSet(ActionSet::GUI).createMWAction(VrControlType::Press, A_MenuSelect, "menu_select", "Menu Select"); + getActionSet(ActionSet::GUI).createMWAction(VrControlType::Press, A_MenuBack, "menu_back", "Menu Back"); + getActionSet(ActionSet::GUI).createMWAction(VrControlType::Hold, MWInput::A_Use, "use", "Use"); + + getActionSet(ActionSet::Tracking).createPoseAction(TrackedLimb::LEFT_HAND, "left_hand_pose", "Left Hand Pose"); + getActionSet(ActionSet::Tracking).createPoseAction(TrackedLimb::RIGHT_HAND, "right_hand_pose", "Right Hand Pose"); + + getActionSet(ActionSet::Haptics).createHapticsAction(TrackedLimb::RIGHT_HAND, "right_hand_haptics", "Right Hand Haptics"); + getActionSet(ActionSet::Haptics).createHapticsAction(TrackedLimb::LEFT_HAND, "left_hand_haptics", "Left Hand Haptics"); + + + auto* xr = Environment::get().getManager(); + auto* trackingManager = Environment::get().getTrackingManager(); + + auto leftHandPath = trackingManager->stringToVRPath("/user/hand/left/input/aim/pose"); + auto rightHandPath = trackingManager->stringToVRPath("/user/hand/right/input/aim/pose"); + + xr->impl().tracker().addTrackingSpace(leftHandPath, getActionSet(ActionSet::Tracking).xrActionSpace(TrackedLimb::LEFT_HAND)); + xr->impl().tracker().addTrackingSpace(rightHandPath, getActionSet(ActionSet::Tracking).xrActionSpace(TrackedLimb::RIGHT_HAND)); + }; + + OpenXRActionSet& OpenXRInput::getActionSet(ActionSet actionSet) + { + auto it = mActionSets.find(actionSet); + if (it == mActionSets.end()) + throw std::logic_error("No such action set"); + return it->second; + } + + void OpenXRInput::suggestBindings(ActionSet actionSet, std::string profilePath, const SuggestedBindings& mwSuggestedBindings) + { + getActionSet(actionSet).suggestBindings(mSuggestedBindings[profilePath], mwSuggestedBindings); + } + + void OpenXRInput::attachActionSets() + { + auto* xr = Environment::get().getManager(); + + // Suggest bindings before attaching + for (auto& profile : mSuggestedBindings) + { + XrPath profilePath = 0; + CHECK_XRCMD( + xrStringToPath(xr->impl().xrInstance(), profile.first.c_str(), &profilePath)); + XrInteractionProfileSuggestedBinding xrProfileSuggestedBindings{ XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING }; + xrProfileSuggestedBindings.interactionProfile = profilePath; + xrProfileSuggestedBindings.suggestedBindings = profile.second.data(); + xrProfileSuggestedBindings.countSuggestedBindings = (uint32_t)profile.second.size(); + CHECK_XRCMD(xrSuggestInteractionProfileBindings(xr->impl().xrInstance(), &xrProfileSuggestedBindings)); + mInteractionProfileNames[profilePath] = profile.first; + mInteractionProfilePaths[profile.first] = profilePath; + } + + // OpenXR requires that xrAttachSessionActionSets be called at most once per session. + // So collect all action sets + std::vector actionSets; + for (auto& actionSet : mActionSets) + actionSets.push_back(actionSet.second.xrActionSet()); + + // Attach + XrSessionActionSetsAttachInfo attachInfo{ XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO }; + attachInfo.countActionSets = actionSets.size(); + attachInfo.actionSets = actionSets.data(); + CHECK_XRCMD(xrAttachSessionActionSets(xr->impl().xrSession(), &attachInfo)); + } + + void OpenXRInput::notifyInteractionProfileChanged() + { + auto xr = MWVR::Environment::get().getManager(); + xr->impl().xrSession(); + + // Unfortunately, openxr does not tell us WHICH profile has changed. + std::array topLevelUserPaths = + { + "/user/hand/left", + "/user/hand/right", + "/user/head", + "/user/gamepad", + "/user/treadmill" + }; + + for (auto& userPath : topLevelUserPaths) + { + auto pathIt = mInteractionProfilePaths.find(userPath); + if (pathIt == mInteractionProfilePaths.end()) + { + XrPath xrUserPath = XR_NULL_PATH; + CHECK_XRCMD( + xrStringToPath(xr->impl().xrInstance(), userPath.c_str(), &xrUserPath)); + mInteractionProfilePaths[userPath] = xrUserPath; + pathIt = mInteractionProfilePaths.find(userPath); + } + + XrInteractionProfileState interactionProfileState{ + XR_TYPE_INTERACTION_PROFILE_STATE + }; + + xrGetCurrentInteractionProfile(xr->impl().xrSession(), pathIt->second, &interactionProfileState); + if (interactionProfileState.interactionProfile) + { + auto activeProfileIt = mActiveInteractionProfiles.find(pathIt->second); + if (activeProfileIt == mActiveInteractionProfiles.end() || interactionProfileState.interactionProfile != activeProfileIt->second) + { + auto activeProfileNameIt = mInteractionProfileNames.find(interactionProfileState.interactionProfile); + Log(Debug::Verbose) << userPath << ": Interaction profile changed to '" << activeProfileNameIt->second << "'"; + mActiveInteractionProfiles[pathIt->second] = interactionProfileState.interactionProfile; + } + } + } + } +} diff --git a/apps/openmw/mwvr/openxrinput.hpp b/apps/openmw/mwvr/openxrinput.hpp new file mode 100644 index 000000000..cc3020d93 --- /dev/null +++ b/apps/openmw/mwvr/openxrinput.hpp @@ -0,0 +1,44 @@ +#ifndef OPENXR_INPUT_HPP +#define OPENXR_INPUT_HPP + +#include "vrinput.hpp" +#include "openxractionset.hpp" + +#include +#include + +namespace MWVR +{ + /// \brief Generates and manages OpenXR Actions and ActionSets by generating openxr bindings from a list of SuggestedBindings structs. + class OpenXRInput + { + public: + using XrSuggestedBindings = std::vector; + using XrProfileSuggestedBindings = std::map; + + //! Default constructor, creates two ActionSets: Gameplay and GUI + OpenXRInput(std::shared_ptr deadzone); + + //! Get the specified actionSet. + OpenXRActionSet& getActionSet(ActionSet actionSet); + + //! Suggest bindings for the specific actionSet and profile pair. Call things after calling attachActionSets is an error. + void suggestBindings(ActionSet actionSet, std::string profile, const SuggestedBindings& mwSuggestedBindings); + + //! Set bindings and attach actionSets to the session. + void attachActionSets(); + + //! Notify that active interaction profile has changed + void notifyInteractionProfileChanged(); + + protected: + std::map mActionSets{}; + std::map mInteractionProfileNames{}; + std::map mInteractionProfilePaths{}; + std::map mActiveInteractionProfiles; + XrProfileSuggestedBindings mSuggestedBindings{}; + bool mAttached = false; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrmanager.cpp b/apps/openmw/mwvr/openxrmanager.cpp new file mode 100644 index 000000000..455d54b48 --- /dev/null +++ b/apps/openmw/mwvr/openxrmanager.cpp @@ -0,0 +1,155 @@ +#include "openxrmanager.hpp" +#include "openxrdebug.hpp" +#include "vrenvironment.hpp" +#include "openxrmanagerimpl.hpp" +#include "../mwinput/inputmanagerimp.hpp" + +#include + +namespace MWVR +{ + OpenXRManager::OpenXRManager() + : mPrivate(nullptr) + , mMutex() + { + + } + + OpenXRManager::~OpenXRManager() + { + + } + + bool + OpenXRManager::realized() const + { + return !!mPrivate; + } + + void OpenXRManager::handleEvents() + { + if (realized()) + return impl().handleEvents(); + } + + FrameInfo OpenXRManager::waitFrame() + { + return impl().waitFrame(); + } + + void OpenXRManager::beginFrame() + { + return impl().beginFrame(); + } + + void OpenXRManager::endFrame(FrameInfo frameInfo, const std::array* layerStack) + { + return impl().endFrame(frameInfo, layerStack); + } + + bool OpenXRManager::appShouldSyncFrameLoop() const + { + if (realized()) + return impl().appShouldSyncFrameLoop(); + return false; + } + + bool OpenXRManager::appShouldRender() const + { + if (realized()) + return impl().appShouldRender(); + return false; + } + + bool OpenXRManager::appShouldReadInput() const + { + if (realized()) + return impl().appShouldReadInput(); + return false; + } + + void + OpenXRManager::realize( + osg::GraphicsContext* gc) + { + lock_guard lock(mMutex); + if (!realized()) + { + gc->makeCurrent(); + mPrivate = std::make_shared(gc); + } + } + + void OpenXRManager::enablePredictions() + { + return impl().enablePredictions(); + } + + void OpenXRManager::disablePredictions() + { + return impl().disablePredictions(); + } + + void OpenXRManager::xrResourceAcquired() + { + return impl().xrResourceAcquired(); + } + + void OpenXRManager::xrResourceReleased() + { + return impl().xrResourceReleased(); + } + + std::array OpenXRManager::getPredictedViews(int64_t predictedDisplayTime, ReferenceSpace space) + { + return impl().getPredictedViews(predictedDisplayTime, space); + } + + MWVR::Pose OpenXRManager::getPredictedHeadPose(int64_t predictedDisplayTime, ReferenceSpace space) + { + return impl().getPredictedHeadPose(predictedDisplayTime, space); + } + + long long OpenXRManager::getLastPredictedDisplayTime() + { + return impl().getLastPredictedDisplayTime(); + } + + long long OpenXRManager::getLastPredictedDisplayPeriod() + { + return impl().getLastPredictedDisplayPeriod(); + } + + std::array OpenXRManager::getRecommendedSwapchainConfig() const + { + return impl().getRecommendedSwapchainConfig(); + } + + bool OpenXRManager::xrExtensionIsEnabled(const char* extensionName) const + { + return impl().xrExtensionIsEnabled(extensionName); + } + + int64_t OpenXRManager::selectColorFormat() + { + return impl().selectColorFormat(); + } + + int64_t OpenXRManager::selectDepthFormat() + { + return impl().selectDepthFormat(); + } + + void OpenXRManager::eraseFormat(int64_t format) + { + return impl().eraseFormat(format); + } + + void + OpenXRManager::CleanupOperation::operator()( + osg::GraphicsContext* gc) + { + // TODO: Use this to make proper cleanup such as cleaning up VRFramebuffers. + } +} + diff --git a/apps/openmw/mwvr/openxrmanager.hpp b/apps/openmw/mwvr/openxrmanager.hpp new file mode 100644 index 000000000..b090dcb5c --- /dev/null +++ b/apps/openmw/mwvr/openxrmanager.hpp @@ -0,0 +1,122 @@ +#ifndef MWVR_OPENRXMANAGER_H +#define MWVR_OPENRXMANAGER_H +#ifndef USE_OPENXR +#error "openxrmanager.hpp included without USE_OPENXR defined" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include "vrtypes.hpp" + + +struct XrSwapchainSubImage; +struct XrCompositionLayerBaseHeader; + +namespace MWVR +{ + class OpenXRManagerImpl; + + /// \brief Manage the openxr runtime and session + class OpenXRManager : public osg::Referenced + { + public: + class CleanupOperation : public osg::GraphicsOperation + { + public: + CleanupOperation() : osg::GraphicsOperation("OpenXRCleanupOperation", false) {}; + void operator()(osg::GraphicsContext* gc) override; + + private: + }; + + + public: + OpenXRManager(); + + ~OpenXRManager(); + + /// Manager has been initialized. + bool realized() const; + + //! Forward call to xrWaitFrame() + FrameInfo waitFrame(); + + //! Forward call to xrBeginFrame() + void beginFrame(); + + //! Forward call to xrEndFrame() + void endFrame(FrameInfo frameInfo, const std::array* layerStack); + + //! Whether the app should call the openxr frame sync functions ( xr*Frame() ) + bool appShouldSyncFrameLoop() const; + + //! Whether the app should render anything. + bool appShouldRender() const; + + //! Whether the session is focused and can read input + bool appShouldReadInput() const; + + //! Process all openxr events + void handleEvents(); + + //! Instantiate implementation + void realize(osg::GraphicsContext* gc); + + //! Enable pose predictions. Exist to police that predictions are never made out of turn. + void enablePredictions(); + + //! Disable pose predictions. + void disablePredictions(); + + //! Must be called every time an openxr resource is acquired to keep track + void xrResourceAcquired(); + + //! Must be called every time an openxr resource is released to keep track + void xrResourceReleased(); + + //! Get poses and fov of both eyes at the predicted time, relative to the given reference space. \note Will throw if predictions are disabled. + std::array getPredictedViews(int64_t predictedDisplayTime, ReferenceSpace space); + + //! Get the pose of the player's head at the predicted time, relative to the given reference space. \note Will throw if predictions are disabled. + MWVR::Pose getPredictedHeadPose(int64_t predictedDisplayTime, ReferenceSpace space); + + //! Last predicted display time returned from xrWaitFrame(); + long long getLastPredictedDisplayTime(); + + //! Last predicted display period returned from xrWaitFrame(); + long long getLastPredictedDisplayPeriod(); + + //! Configuration hints for instantiating swapchains, queried from openxr. + std::array getRecommendedSwapchainConfig() const; + + //! Check whether a given openxr extension is enabled or not + bool xrExtensionIsEnabled(const char* extensionName) const; + + //! Selects a color format from among formats offered by the runtime + //! Returns 0 if no format is supported. + int64_t selectColorFormat(); + + //! Selects a depth format from among formats offered by the runtime + //! Returns 0 if no format is supported. + int64_t selectDepthFormat(); + + //! Erase format from list of format candidates + void eraseFormat(int64_t format); + + OpenXRManagerImpl& impl() { return *mPrivate; } + const OpenXRManagerImpl& impl() const { return *mPrivate; } + + private: + std::shared_ptr mPrivate; + std::mutex mMutex; + using lock_guard = std::lock_guard; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrmanagerimpl.cpp b/apps/openmw/mwvr/openxrmanagerimpl.cpp new file mode 100644 index 000000000..5376df789 --- /dev/null +++ b/apps/openmw/mwvr/openxrmanagerimpl.cpp @@ -0,0 +1,686 @@ +#include "openxrmanagerimpl.hpp" +#include "openxrdebug.hpp" +#include "openxrplatform.hpp" +#include "openxrswapchain.hpp" +#include "openxrswapchainimpl.hpp" +#include "openxrtypeconversions.hpp" +#include "vrenvironment.hpp" +#include "vrinputmanager.hpp" + +#include +#include +#include + +#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 + +#include + +#include +#include +#include + +#define ENUM_CASE_STR(name, val) case name: return #name; +#define MAKE_TO_STRING_FUNC(enumType) \ + inline const char* to_string(enumType e) { \ + switch (e) { \ + XR_LIST_ENUM_##enumType(ENUM_CASE_STR) \ + default: return "Unknown " #enumType; \ + } \ + } + +MAKE_TO_STRING_FUNC(XrReferenceSpaceType); +MAKE_TO_STRING_FUNC(XrViewConfigurationType); +MAKE_TO_STRING_FUNC(XrEnvironmentBlendMode); +MAKE_TO_STRING_FUNC(XrSessionState); +MAKE_TO_STRING_FUNC(XrResult); +MAKE_TO_STRING_FUNC(XrFormFactor); +MAKE_TO_STRING_FUNC(XrStructureType); + +namespace MWVR +{ + OpenXRManagerImpl::OpenXRManagerImpl(osg::GraphicsContext* gc) + : mPlatform(gc) + { + mInstance = mPlatform.createXrInstance("openmw_vr"); + + LogInstanceInfo(); + + setupDebugMessenger(); + + setupLayerDepth(); + + getSystem(); + + enumerateViews(); + + // TODO: Blend mode + // setupBlendMode(); + + mSession = mPlatform.createXrSession(mInstance, mSystemId); + + LogReferenceSpaces(); + + createReferenceSpaces(); + + initTracker(); + + getSystemProperties(); + } + + void OpenXRManagerImpl::createReferenceSpaces() + { + XrReferenceSpaceCreateInfo createInfo{ XR_TYPE_REFERENCE_SPACE_CREATE_INFO }; + createInfo.poseInReferenceSpace.orientation.w = 1.f; // Identity pose + createInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW; + CHECK_XRCMD(xrCreateReferenceSpace(mSession, &createInfo, &mReferenceSpaceView)); + createInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_STAGE; + CHECK_XRCMD(xrCreateReferenceSpace(mSession, &createInfo, &mReferenceSpaceStage)); + createInfo.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + CHECK_XRCMD(xrCreateReferenceSpace(mSession, &createInfo, &mReferenceSpaceLocal)); + } + + void OpenXRManagerImpl::getSystem() + { + XrSystemGetInfo systemInfo{ XR_TYPE_SYSTEM_GET_INFO }; + systemInfo.formFactor = mFormFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + auto res = CHECK_XRCMD(xrGetSystem(mInstance, &systemInfo, &mSystemId)); + if (!XR_SUCCEEDED(res)) + mPlatform.initFailure(res, mInstance); + } + + void OpenXRManagerImpl::getSystemProperties() + {// Read and log graphics properties for the swapchain + CHECK_XRCMD(xrGetSystemProperties(mInstance, mSystemId, &mSystemProperties)); + + // Log system properties. + { + std::stringstream ss; + ss << "System Properties: Name=" << mSystemProperties.systemName << " VendorId=" << mSystemProperties.vendorId << std::endl; + ss << "System Graphics Properties: MaxWidth=" << mSystemProperties.graphicsProperties.maxSwapchainImageWidth; + ss << " MaxHeight=" << mSystemProperties.graphicsProperties.maxSwapchainImageHeight; + ss << " MaxLayers=" << mSystemProperties.graphicsProperties.maxLayerCount << std::endl; + ss << "System Tracking Properties: OrientationTracking=" << mSystemProperties.trackingProperties.orientationTracking ? "True" : "False"; + ss << " PositionTracking=" << mSystemProperties.trackingProperties.positionTracking ? "True" : "False"; + Log(Debug::Verbose) << ss.str(); + } + } + + void OpenXRManagerImpl::enumerateViews() + { + uint32_t viewCount = 0; + CHECK_XRCMD(xrEnumerateViewConfigurationViews(mInstance, mSystemId, mViewConfigType, 2, &viewCount, mConfigViews.data())); + + if (viewCount != 2) + { + std::stringstream ss; + ss << "xrEnumerateViewConfigurationViews returned " << viewCount << " views"; + Log(Debug::Verbose) << ss.str(); + } + } + + void OpenXRManagerImpl::setupLayerDepth() + { + // Layer depth is enabled, cache the invariant values + if (xrExtensionIsEnabled(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME)) + { + GLfloat depthRange[2] = { 0.f, 1.f }; + glGetFloatv(GL_DEPTH_RANGE, depthRange); + auto nearClip = Settings::Manager::getFloat("near clip", "Camera"); + + for (auto& layer : mLayerDepth) + { + layer.type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR; + layer.next = nullptr; + layer.minDepth = depthRange[0]; + layer.maxDepth = depthRange[1]; + layer.nearZ = nearClip; + } + } + } + + std::string XrResultString(XrResult res) + { + return to_string(res); + } + + OpenXRManagerImpl::~OpenXRManagerImpl() + { + + } + + void OpenXRManagerImpl::setupExtensionsAndLayers() + { + + } + static XrBool32 xrDebugCallback( + XrDebugUtilsMessageSeverityFlagsEXT messageSeverity, + XrDebugUtilsMessageTypeFlagsEXT messageType, + const XrDebugUtilsMessengerCallbackDataEXT* callbackData, + void* userData) + { + OpenXRManagerImpl* manager = reinterpret_cast(userData); + (void)manager; + std::string severityStr = ""; + std::string typeStr = ""; + + switch (messageSeverity) + { + case XR_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: + severityStr = "Verbose"; break; + case XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: + severityStr = "Info"; break; + case XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: + severityStr = "Warning"; break; + case XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: + severityStr = "Error"; break; + default: + severityStr = "Unknown"; break; + } + + switch (messageType) + { + case XR_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT: + typeStr = "General"; break; + case XR_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT: + typeStr = "Validation"; break; + case XR_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT: + typeStr = "Performance"; break; + case XR_DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT: + typeStr = "Conformance"; break; + default: + typeStr = "Unknown"; break; + } + + Log(Debug::Verbose) << "XrCallback: [" << severityStr << "][" << typeStr << "][ID=" << (callbackData->messageId ? callbackData->messageId : "null") << "]: " << callbackData->message; + + return XR_FALSE; + } + + void OpenXRManagerImpl::setupDebugMessenger(void) + { + if (xrExtensionIsEnabled(XR_EXT_DEBUG_UTILS_EXTENSION_NAME)) + { + XrDebugUtilsMessengerCreateInfoEXT createInfo{ XR_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, nullptr }; + + // Debug message severity levels + if (Settings::Manager::getBool("XR_EXT_debug_utils message level verbose", "VR Debug")) + createInfo.messageSeverities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT; + if (Settings::Manager::getBool("XR_EXT_debug_utils message level info", "VR Debug")) + createInfo.messageSeverities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT; + if (Settings::Manager::getBool("XR_EXT_debug_utils message level warning", "VR Debug")) + createInfo.messageSeverities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT; + if (Settings::Manager::getBool("XR_EXT_debug_utils message level error", "VR Debug")) + createInfo.messageSeverities |= XR_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + + // Debug message types + if (Settings::Manager::getBool("XR_EXT_debug_utils message type general", "VR Debug")) + createInfo.messageTypes |= XR_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT; + if (Settings::Manager::getBool("XR_EXT_debug_utils message type validation", "VR Debug")) + createInfo.messageTypes |= XR_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT; + if (Settings::Manager::getBool("XR_EXT_debug_utils message type performance", "VR Debug")) + createInfo.messageTypes |= XR_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + if (Settings::Manager::getBool("XR_EXT_debug_utils message type conformance", "VR Debug")) + createInfo.messageTypes |= XR_DEBUG_UTILS_MESSAGE_TYPE_CONFORMANCE_BIT_EXT; + + createInfo.userCallback = &xrDebugCallback; + createInfo.userData = this; + + PFN_xrCreateDebugUtilsMessengerEXT createDebugUtilsMessenger = reinterpret_cast(xrGetFunction("xrCreateDebugUtilsMessengerEXT")); + assert(createDebugUtilsMessenger); + CHECK_XRCMD(createDebugUtilsMessenger(mInstance, &createInfo, &mDebugMessenger)); + } + } + + void + OpenXRManagerImpl::LogInstanceInfo() { + + XrInstanceProperties instanceProperties{ XR_TYPE_INSTANCE_PROPERTIES }; + CHECK_XRCMD(xrGetInstanceProperties(mInstance, &instanceProperties)); + Log(Debug::Verbose) << "Instance RuntimeName=" << instanceProperties.runtimeName << " RuntimeVersion=" << instanceProperties.runtimeVersion; + } + + void + OpenXRManagerImpl::LogReferenceSpaces() { + + uint32_t spaceCount = 0; + CHECK_XRCMD(xrEnumerateReferenceSpaces(mSession, 0, &spaceCount, nullptr)); + std::vector spaces(spaceCount); + CHECK_XRCMD(xrEnumerateReferenceSpaces(mSession, spaceCount, &spaceCount, spaces.data())); + + std::stringstream ss; + ss << "Available reference spaces=" << spaceCount << std::endl; + + for (XrReferenceSpaceType space : spaces) + ss << " Name: " << to_string(space) << std::endl; + Log(Debug::Verbose) << ss.str(); + } + + FrameInfo + OpenXRManagerImpl::waitFrame() + { + XrFrameWaitInfo frameWaitInfo{ XR_TYPE_FRAME_WAIT_INFO }; + XrFrameState frameState{ XR_TYPE_FRAME_STATE }; + + CHECK_XRCMD(xrWaitFrame(mSession, &frameWaitInfo, &frameState)); + mFrameState = frameState; + + FrameInfo frameInfo; + + frameInfo.runtimePredictedDisplayTime = mFrameState.predictedDisplayTime; + frameInfo.runtimePredictedDisplayPeriod = mFrameState.predictedDisplayPeriod; + frameInfo.runtimeRequestsRender = !!mFrameState.shouldRender; + + return frameInfo; + } + + void + OpenXRManagerImpl::beginFrame() + { + XrFrameBeginInfo frameBeginInfo{ XR_TYPE_FRAME_BEGIN_INFO }; + CHECK_XRCMD(xrBeginFrame(mSession, &frameBeginInfo)); + } + + void + OpenXRManagerImpl::endFrame(FrameInfo frameInfo, const std::array* layerStack) + { + std::array compositionLayerProjectionViews{}; + XrCompositionLayerProjection layer{}; + std::array compositionLayerDepth{}; + XrFrameEndInfo frameEndInfo{ XR_TYPE_FRAME_END_INFO }; + frameEndInfo.displayTime = frameInfo.runtimePredictedDisplayTime; + frameEndInfo.environmentBlendMode = mEnvironmentBlendMode; + if (layerStack && frameInfo.runtimeRequestsRender) + { + compositionLayerProjectionViews[(int)Side::LEFT_SIDE] = toXR((*layerStack)[(int)Side::LEFT_SIDE]); + compositionLayerProjectionViews[(int)Side::RIGHT_SIDE] = toXR((*layerStack)[(int)Side::RIGHT_SIDE]); + layer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION; + layer.space = mReferenceSpaceStage; + layer.viewCount = 2; + layer.views = compositionLayerProjectionViews.data(); + auto* xrLayerStack = reinterpret_cast(&layer); + + if (xrExtensionIsEnabled(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME)) + { + auto farClip = Settings::Manager::getFloat("viewing distance", "Camera"); + // All values not set here are set previously as they are constant + compositionLayerDepth = mLayerDepth; + compositionLayerDepth[(int)Side::LEFT_SIDE].farZ = farClip; + compositionLayerDepth[(int)Side::RIGHT_SIDE].farZ = farClip; + compositionLayerDepth[(int)Side::LEFT_SIDE].subImage = toXR((*layerStack)[(int)Side::LEFT_SIDE].subImage, true); + compositionLayerDepth[(int)Side::RIGHT_SIDE].subImage = toXR((*layerStack)[(int)Side::RIGHT_SIDE].subImage, true); + if (compositionLayerDepth[(int)Side::LEFT_SIDE].subImage.swapchain != XR_NULL_HANDLE + && compositionLayerDepth[(int)Side::RIGHT_SIDE].subImage.swapchain != XR_NULL_HANDLE) + { + compositionLayerProjectionViews[(int)Side::LEFT_SIDE].next = &compositionLayerDepth[(int)Side::LEFT_SIDE]; + compositionLayerProjectionViews[(int)Side::RIGHT_SIDE].next = &compositionLayerDepth[(int)Side::RIGHT_SIDE]; + } + } + frameEndInfo.layerCount = 1; + frameEndInfo.layers = &xrLayerStack; + } + else + { + frameEndInfo.layerCount = 0; + frameEndInfo.layers = nullptr; + } + CHECK_XRCMD(xrEndFrame(mSession, &frameEndInfo)); + } + + std::array + OpenXRManagerImpl::getPredictedViews( + int64_t predictedDisplayTime, + ReferenceSpace space) + { + //if (!mPredictionsEnabled) + //{ + // Log(Debug::Error) << "Prediction out of order"; + // throw std::logic_error("Prediction out of order"); + //} + std::array xrViews{ {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}} }; + XrViewState viewState{ XR_TYPE_VIEW_STATE }; + uint32_t viewCount = 2; + + XrViewLocateInfo viewLocateInfo{ XR_TYPE_VIEW_LOCATE_INFO }; + viewLocateInfo.viewConfigurationType = mViewConfigType; + viewLocateInfo.displayTime = predictedDisplayTime; + switch (space) + { + case ReferenceSpace::STAGE: + viewLocateInfo.space = mReferenceSpaceStage; + break; + case ReferenceSpace::VIEW: + viewLocateInfo.space = mReferenceSpaceView; + break; + } + CHECK_XRCMD(xrLocateViews(mSession, &viewLocateInfo, &viewState, viewCount, &viewCount, xrViews.data())); + + std::array vrViews{}; + vrViews[(int)Side::LEFT_SIDE].pose = fromXR(xrViews[(int)Side::LEFT_SIDE].pose); + vrViews[(int)Side::RIGHT_SIDE].pose = fromXR(xrViews[(int)Side::RIGHT_SIDE].pose); + vrViews[(int)Side::LEFT_SIDE].fov = fromXR(xrViews[(int)Side::LEFT_SIDE].fov); + vrViews[(int)Side::RIGHT_SIDE].fov = fromXR(xrViews[(int)Side::RIGHT_SIDE].fov); + return vrViews; + } + + MWVR::Pose OpenXRManagerImpl::getPredictedHeadPose( + int64_t predictedDisplayTime, + ReferenceSpace space) + { + if (!mPredictionsEnabled) + { + Log(Debug::Error) << "Prediction out of order"; + throw std::logic_error("Prediction out of order"); + } + XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION }; + XrSpace limbSpace = mReferenceSpaceView; + XrSpace referenceSpace = XR_NULL_HANDLE; + + switch (space) + { + case ReferenceSpace::STAGE: + referenceSpace = mReferenceSpaceStage; + break; + case ReferenceSpace::VIEW: + referenceSpace = mReferenceSpaceView; + break; + } + CHECK_XRCMD(xrLocateSpace(limbSpace, referenceSpace, predictedDisplayTime, &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 valid + location.pose.orientation.w = 1; + } + return MWVR::Pose{ + fromXR(location.pose.position), + fromXR(location.pose.orientation) + }; + } + + void OpenXRManagerImpl::handleEvents() + { + std::unique_lock lock(mMutex); + + xrQueueEvents(); + + while (auto* event = nextEvent()) + { + if (!processEvent(event)) + { + // Do not consider processing an event optional. + // Retry once per frame until every event has been successfully processed + return; + } + popEvent(); + } + + if (mXrSessionShouldStop) + { + if (checkStopCondition()) + { + CHECK_XRCMD(xrEndSession(mSession)); + mXrSessionShouldStop = false; + } + } + } + + const XrEventDataBaseHeader* OpenXRManagerImpl::nextEvent() + { + if (mEventQueue.size() > 0) + return reinterpret_cast (&mEventQueue.front()); + return nullptr; + } + + bool OpenXRManagerImpl::processEvent(const XrEventDataBaseHeader* header) + { + Log(Debug::Verbose) << "OpenXR: Event received: " << to_string(header->type); + switch (header->type) + { + case XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED: + { + const auto* stateChangeEvent = reinterpret_cast(header); + return handleSessionStateChanged(*stateChangeEvent); + break; + } + case XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED: + MWVR::Environment::get().getInputManager()->notifyInteractionProfileChanged(); + break; + case XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING: + case XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING: + default: + { + Log(Debug::Verbose) << "OpenXR: Event ignored"; + break; + } + } + return true; + } + + bool + OpenXRManagerImpl::handleSessionStateChanged( + const XrEventDataSessionStateChanged& stateChangedEvent) + { + Log(Debug::Verbose) << "XrEventDataSessionStateChanged: state " << to_string(mSessionState) << "->" << to_string(stateChangedEvent.state); + mSessionState = stateChangedEvent.state; + + // Ref: https://www.khronos.org/registry/OpenXR/specs/1.0/html/xrspec.html#session-states + switch (mSessionState) + { + case XR_SESSION_STATE_IDLE: + { + mAppShouldSyncFrameLoop = false; + mAppShouldRender = false; + mAppShouldReadInput = false; + mXrSessionShouldStop = false; + break; + } + case XR_SESSION_STATE_READY: + { + mAppShouldSyncFrameLoop = true; + mAppShouldRender = false; + mAppShouldReadInput = false; + mXrSessionShouldStop = false; + + XrSessionBeginInfo beginInfo{ XR_TYPE_SESSION_BEGIN_INFO }; + beginInfo.primaryViewConfigurationType = mViewConfigType; + CHECK_XRCMD(xrBeginSession(mSession, &beginInfo)); + + break; + } + case XR_SESSION_STATE_STOPPING: + { + mAppShouldSyncFrameLoop = false; + mAppShouldRender = false; + mAppShouldReadInput = false; + mXrSessionShouldStop = true; + break; + } + case XR_SESSION_STATE_SYNCHRONIZED: + { + mAppShouldSyncFrameLoop = true; + mAppShouldRender = false; + mAppShouldReadInput = false; + mXrSessionShouldStop = false; + break; + } + case XR_SESSION_STATE_VISIBLE: + { + mAppShouldSyncFrameLoop = true; + mAppShouldRender = true; + mAppShouldReadInput = false; + mXrSessionShouldStop = false; + break; + } + case XR_SESSION_STATE_FOCUSED: + { + mAppShouldSyncFrameLoop = true; + mAppShouldRender = true; + mAppShouldReadInput = true; + mXrSessionShouldStop = false; + break; + } + default: + Log(Debug::Warning) << "XrEventDataSessionStateChanged: Ignoring new state " << to_string(mSessionState); + } + + return true; + } + + bool OpenXRManagerImpl::checkStopCondition() + { + return mAcquiredResources == 0; + } + + bool OpenXRManagerImpl::xrNextEvent(XrEventDataBuffer& eventBuffer) + { + XrEventDataBaseHeader* baseHeader = reinterpret_cast(&eventBuffer); + *baseHeader = { XR_TYPE_EVENT_DATA_BUFFER }; + const XrResult result = xrPollEvent(mInstance, &eventBuffer); + if (result == XR_SUCCESS) + { + if (baseHeader->type == XR_TYPE_EVENT_DATA_EVENTS_LOST) { + const XrEventDataEventsLost* const eventsLost = reinterpret_cast(baseHeader); + Log(Debug::Warning) << "OpenXRManagerImpl: Lost " << eventsLost->lostEventCount << " events"; + } + + return baseHeader; + } + + if (result != XR_EVENT_UNAVAILABLE) + CHECK_XRRESULT(result, "xrPollEvent"); + return false; + } + + void OpenXRManagerImpl::popEvent() + { + if (mEventQueue.size() > 0) + mEventQueue.pop(); + } + + void + OpenXRManagerImpl::xrQueueEvents() + { + XrEventDataBuffer eventBuffer; + while (xrNextEvent(eventBuffer)) + { + mEventQueue.push(eventBuffer); + } + } + + bool OpenXRManagerImpl::xrExtensionIsEnabled(const char* extensionName) const + { + return mPlatform.extensionEnabled(extensionName); + } + + void OpenXRManagerImpl::xrResourceAcquired() + { + std::unique_lock lock(mMutex); + mAcquiredResources++; + } + + void OpenXRManagerImpl::xrResourceReleased() + { + std::unique_lock lock(mMutex); + if (mAcquiredResources == 0) + throw std::logic_error("Releasing a nonexistent resource"); + mAcquiredResources--; + } + + void OpenXRManagerImpl::xrUpdateNames() + { + VrDebug::setName(mInstance, "OpenMW XR Instance"); + VrDebug::setName(mSession, "OpenMW XR Session"); + VrDebug::setName(mReferenceSpaceStage, "OpenMW XR Reference Space Stage"); + VrDebug::setName(mReferenceSpaceView, "OpenMW XR Reference Space Stage"); + } + + PFN_xrVoidFunction OpenXRManagerImpl::xrGetFunction(const std::string& name) + { + PFN_xrVoidFunction function = nullptr; + xrGetInstanceProcAddr(mInstance, name.c_str(), &function); + return function; + } + + int64_t OpenXRManagerImpl::selectColorFormat() + { + // Find supported color swapchain format. + return mPlatform.selectColorFormat(); + } + + int64_t OpenXRManagerImpl::selectDepthFormat() + { + // Find supported depth swapchain format. + return mPlatform.selectDepthFormat(); + } + + void OpenXRManagerImpl::eraseFormat(int64_t format) + { + mPlatform.eraseFormat(format); + } + + void OpenXRManagerImpl::initTracker() + { + auto* trackingManager = Environment::get().getTrackingManager(); + auto headPath = trackingManager->stringToVRPath("/user/head/input/pose"); + + mTracker.reset(new OpenXRTracker("pcstage", mReferenceSpaceStage)); + mTracker->addTrackingSpace(headPath, mReferenceSpaceView); + mTrackerToWorldBinding.reset(new VRTrackingToWorldBinding("pcworld", mTracker.get(), headPath)); + } + + void OpenXRManagerImpl::enablePredictions() + { + mPredictionsEnabled = true; + } + + void OpenXRManagerImpl::disablePredictions() + { + mPredictionsEnabled = false; + } + + long long OpenXRManagerImpl::getLastPredictedDisplayTime() + { + return mFrameState.predictedDisplayTime; + } + + long long OpenXRManagerImpl::getLastPredictedDisplayPeriod() + { + return mFrameState.predictedDisplayPeriod; + } + std::array OpenXRManagerImpl::getRecommendedSwapchainConfig() const + { + std::array config{}; + for (uint32_t i = 0; i < 2; i++) + config[i] = SwapchainConfig{ + (int)mConfigViews[i].recommendedImageRectWidth, + (int)mConfigViews[i].recommendedImageRectHeight, + (int)mConfigViews[i].recommendedSwapchainSampleCount, + (int)mConfigViews[i].maxImageRectWidth, + (int)mConfigViews[i].maxImageRectHeight, + (int)mConfigViews[i].maxSwapchainSampleCount, + }; + return config; + } + XrSpace OpenXRManagerImpl::getReferenceSpace(ReferenceSpace space) + { + switch (space) + { + case ReferenceSpace::STAGE: + return mReferenceSpaceStage; + case ReferenceSpace::VIEW: + return mReferenceSpaceView; + } + return XR_NULL_HANDLE; + } +} + diff --git a/apps/openmw/mwvr/openxrmanagerimpl.hpp b/apps/openmw/mwvr/openxrmanagerimpl.hpp new file mode 100644 index 000000000..e2b4eb19b --- /dev/null +++ b/apps/openmw/mwvr/openxrmanagerimpl.hpp @@ -0,0 +1,115 @@ +#ifndef OPENXR_MANAGER_IMPL_HPP +#define OPENXR_MANAGER_IMPL_HPP + +#include "openxrmanager.hpp" +#include "openxrplatform.hpp" +#include "openxrtracker.hpp" +#include "../mwinput/inputmanagerimp.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace MWVR +{ + /// \brief Implementation of OpenXRManager + class OpenXRManagerImpl + { + public: + OpenXRManagerImpl(osg::GraphicsContext* gc); + ~OpenXRManagerImpl(void); + + FrameInfo waitFrame(); + void beginFrame(); + void endFrame(FrameInfo frameInfo, const std::array* layerStack); + bool appShouldSyncFrameLoop() const { return mAppShouldSyncFrameLoop; } + bool appShouldRender() const { return mAppShouldRender; } + bool appShouldReadInput() const { return mAppShouldReadInput; } + 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; }; + bool xrExtensionIsEnabled(const char* extensionName) const; + void xrResourceAcquired(); + void xrResourceReleased(); + void xrUpdateNames(); + PFN_xrVoidFunction xrGetFunction(const std::string& name); + int64_t selectColorFormat(); + int64_t selectDepthFormat(); + void eraseFormat(int64_t format); + OpenXRPlatform& platform() { return mPlatform; } + OpenXRTracker& tracker() { return *mTracker; } + void initTracker(); + + protected: + void setupExtensionsAndLayers(); + void setupDebugMessenger(void); + void LogInstanceInfo(); + void LogReferenceSpaces(); + bool xrNextEvent(XrEventDataBuffer& eventBuffer); + void xrQueueEvents(); + const XrEventDataBaseHeader* nextEvent(); + bool processEvent(const XrEventDataBaseHeader* header); + void popEvent(); + bool handleSessionStateChanged(const XrEventDataSessionStateChanged& stateChangedEvent); + bool checkStopCondition(); + void createReferenceSpaces(); + void getSystem(); + void getSystemProperties(); + void enumerateViews(); + void setupLayerDepth(); + + 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; + 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; + XrSpace mReferenceSpaceLocal = XR_NULL_HANDLE; + XrFrameState mFrameState{}; + XrSessionState mSessionState = XR_SESSION_STATE_UNKNOWN; + XrDebugUtilsMessengerEXT mDebugMessenger{ nullptr }; + OpenXRPlatform mPlatform; + + std::unique_ptr mTracker{ nullptr }; + std::unique_ptr mTrackerToWorldBinding{ nullptr }; + + bool mXrSessionShouldStop = false; + bool mAppShouldSyncFrameLoop = false; + bool mAppShouldRender = false; + bool mAppShouldReadInput = false; + + uint32_t mAcquiredResources = 0; + std::mutex mMutex{}; + std::queue mEventQueue; + + std::array mLayerDepth; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrplatform.cpp b/apps/openmw/mwvr/openxrplatform.cpp new file mode 100644 index 000000000..43f03e298 --- /dev/null +++ b/apps/openmw/mwvr/openxrplatform.cpp @@ -0,0 +1,746 @@ +#include "openxrswapchainimage.hpp" +#include "openxrmanagerimpl.hpp" +#include "openxrplatform.hpp" +#include "vrenvironment.hpp" + +// The OpenXR SDK's platform headers assume we've included platform headers +#ifdef _WIN32 +#include +#include + +#ifdef XR_USE_GRAPHICS_API_D3D11 +#include +#endif + +#elif __linux__ +#include +#include +#undef None + +#else +#error Unsupported platform +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace MWVR +{ + + XrResult CheckXrResult(XrResult res, const char* originator, const char* sourceLocation) { + static bool initialized = false; + static bool sLogAllXrCalls = false; + static bool sContinueOnErrors = false; + if (!initialized) + { + initialized = true; + sLogAllXrCalls = Settings::Manager::getBool("log all openxr calls", "VR Debug"); + sContinueOnErrors = Settings::Manager::getBool("continue on errors", "VR Debug"); + } + + auto resultString = XrResultString(res); + + if (XR_FAILED(res)) { + std::stringstream ss; +#ifdef _WIN32 + ss << sourceLocation << ": OpenXR[Error: " << resultString << "][Thread: " << std::this_thread::get_id() << "]: " << originator; +#elif __linux__ + ss << sourceLocation << ": OpenXR[Error: " << resultString << "][Thread: " << std::this_thread::get_id() << "]: " << originator; +#endif + Log(Debug::Error) << ss.str(); + if (!sContinueOnErrors) + throw std::runtime_error(ss.str().c_str()); + } + else if (res != XR_SUCCESS || sLogAllXrCalls) + { +#ifdef _WIN32 + Log(Debug::Verbose) << sourceLocation << ": OpenXR[" << resultString << "][" << std::this_thread::get_id() << "]: " << originator; +#elif __linux__ + Log(Debug::Verbose) << sourceLocation << ": OpenXR[" << resultString << "][" << std::this_thread::get_id() << "]: " << originator; +#endif + } + + return res; + } + + void OpenXRPlatform::enumerateExtensions(const char* layerName, int logIndent) + { + uint32_t extensionCount = 0; + std::vector availableExtensions; + CHECK_XRCMD(xrEnumerateInstanceExtensionProperties(layerName, 0, &extensionCount, nullptr)); + availableExtensions.resize(extensionCount, XrExtensionProperties{ XR_TYPE_EXTENSION_PROPERTIES }); + CHECK_XRCMD(xrEnumerateInstanceExtensionProperties(layerName, availableExtensions.size(), &extensionCount, availableExtensions.data())); + + std::vector extensionNames; + const std::string indentStr(logIndent, ' '); + for (auto& extension : availableExtensions) + { + if (layerName) + mAvailableLayerExtensions[layerName][extension.extensionName] = extension; + else + mAvailableExtensions[extension.extensionName] = extension; + + Log(Debug::Verbose) << indentStr << "Name=" << extension.extensionName << " SpecVersion=" << extension.extensionVersion; + } + } + + struct OpenXRPlatformPrivate + { + OpenXRPlatformPrivate(osg::GraphicsContext* gc); + ~OpenXRPlatformPrivate(); + +#ifdef XR_USE_GRAPHICS_API_D3D11 + typedef BOOL(WINAPI* P_wglDXSetResourceShareHandleNV)(void* dxObject, HANDLE shareHandle); + typedef HANDLE(WINAPI* P_wglDXOpenDeviceNV)(void* dxDevice); + typedef BOOL(WINAPI* P_wglDXCloseDeviceNV)(HANDLE hDevice); + typedef HANDLE(WINAPI* P_wglDXRegisterObjectNV)(HANDLE hDevice, void* dxObject, + GLuint name, GLenum type, GLenum access); + typedef BOOL(WINAPI* P_wglDXUnregisterObjectNV)(HANDLE hDevice, HANDLE hObject); + typedef BOOL(WINAPI* P_wglDXObjectAccessNV)(HANDLE hObject, GLenum access); + typedef BOOL(WINAPI* P_wglDXLockObjectsNV)(HANDLE hDevice, GLint count, HANDLE* hObjects); + typedef BOOL(WINAPI* P_wglDXUnlockObjectsNV)(HANDLE hDevice, GLint count, HANDLE* hObjects); + + void initializeD3D11(XrGraphicsRequirementsD3D11KHR requirements) + { + mD3D11Dll = LoadLibrary("D3D11.dll"); + + if (!mD3D11Dll) + throw std::runtime_error("Current OpenXR runtime requires DirectX >= 11.0 but D3D11.dll was not found."); + + pD3D11CreateDevice = reinterpret_cast(GetProcAddress(mD3D11Dll, "D3D11CreateDevice")); + + if (!pD3D11CreateDevice) + throw std::runtime_error("Symbol 'D3D11CreateDevice' not found in D3D11.dll"); + + // Create the device and device context objects + pD3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_HARDWARE, + nullptr, + 0, + nullptr, + 0, + D3D11_SDK_VERSION, + &mD3D11Device, + nullptr, + &mD3D11ImmediateContext); + + mD3D11bindings.device = mD3D11Device; + + //typedef HANDLE (WINAPI* P_wglDXOpenDeviceNV)(void* dxDevice); + //P_wglDXOpenDeviceNV wglDXOpenDeviceNV = reinterpret_cast(wglGetProcAddress("wglDXOpenDeviceNV")); + //P_wglDXOpenDeviceNV wglDXOpenDeviceNV = reinterpret_cast(wglGetProcAddress("wglDXOpenDeviceNV")); + +#define LOAD_WGL(a) a = reinterpret_cast(wglGetProcAddress(#a)); if(!a) throw std::runtime_error("Extension WGL_NV_DX_interop2 required to run OpenMW VR via DirectX missing expected symbol '" #a "'.") + LOAD_WGL(wglDXSetResourceShareHandleNV); + LOAD_WGL(wglDXOpenDeviceNV); + LOAD_WGL(wglDXCloseDeviceNV); + LOAD_WGL(wglDXRegisterObjectNV); + LOAD_WGL(wglDXUnregisterObjectNV); + LOAD_WGL(wglDXObjectAccessNV); + LOAD_WGL(wglDXLockObjectsNV); + LOAD_WGL(wglDXUnlockObjectsNV); +#undef LOAD_WGL + + wglDXDevice = wglDXOpenDeviceNV(mD3D11Device); + } + + bool mWGL_NV_DX_interop2 = false; + XrGraphicsBindingD3D11KHR mD3D11bindings{ XR_TYPE_GRAPHICS_BINDING_D3D11_KHR }; + ID3D11Device* mD3D11Device = nullptr; + ID3D11DeviceContext* mD3D11ImmediateContext = nullptr; + HMODULE mD3D11Dll = nullptr; + PFN_D3D11_CREATE_DEVICE pD3D11CreateDevice = nullptr; + + P_wglDXSetResourceShareHandleNV wglDXSetResourceShareHandleNV = nullptr; + P_wglDXOpenDeviceNV wglDXOpenDeviceNV = nullptr; + P_wglDXCloseDeviceNV wglDXCloseDeviceNV = nullptr; + P_wglDXRegisterObjectNV wglDXRegisterObjectNV = nullptr; + P_wglDXUnregisterObjectNV wglDXUnregisterObjectNV = nullptr; + P_wglDXObjectAccessNV wglDXObjectAccessNV = nullptr; + P_wglDXLockObjectsNV wglDXLockObjectsNV = nullptr; + P_wglDXUnlockObjectsNV wglDXUnlockObjectsNV = nullptr; + + HANDLE wglDXDevice = nullptr; +#endif + }; + + OpenXRPlatformPrivate::OpenXRPlatformPrivate(osg::GraphicsContext* gc) + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + typedef const char* (WINAPI* PFNWGLGETEXTENSIONSSTRINGARBPROC) (HDC hdc); + PFNWGLGETEXTENSIONSSTRINGARBPROC wglGetExtensionsStringARB = 0; + wglGetExtensionsStringARB = reinterpret_cast(wglGetProcAddress("wglGetExtensionsStringARB")); + if (wglGetExtensionsStringARB) + { + std::string wglExtensions = wglGetExtensionsStringARB(wglGetCurrentDC()); + Log(Debug::Verbose) << "WGL Extensions: " << wglExtensions; + mWGL_NV_DX_interop2 = wglExtensions.find("WGL_NV_DX_interop2") != std::string::npos; + } + else + Log(Debug::Verbose) << "Unable to query WGL extensions"; +#endif + } + + OpenXRPlatformPrivate::~OpenXRPlatformPrivate() + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + if (wglDXDevice) + wglDXCloseDeviceNV(wglDXDevice); + if (mD3D11ImmediateContext) + mD3D11ImmediateContext->Release(); + if (mD3D11Device) + mD3D11Device->Release(); + if (mD3D11Dll) + FreeLibrary(mD3D11Dll); +#endif + } + + OpenXRPlatform::OpenXRPlatform(osg::GraphicsContext* gc) + : mPrivate(new OpenXRPlatformPrivate(gc)) + { + // Enumerate layers and their extensions. + uint32_t layerCount; + CHECK_XRCMD(xrEnumerateApiLayerProperties(0, &layerCount, nullptr)); + std::vector layers(layerCount, XrApiLayerProperties{ XR_TYPE_API_LAYER_PROPERTIES }); + CHECK_XRCMD(xrEnumerateApiLayerProperties((uint32_t)layers.size(), &layerCount, layers.data())); + + Log(Debug::Verbose) << "Available Extensions: "; + enumerateExtensions(nullptr, 2); + Log(Debug::Verbose) << "Available Layers: "; + + if (layers.size() == 0) + { + Log(Debug::Verbose) << " No layers available"; + } + for (const XrApiLayerProperties& layer : layers) { + Log(Debug::Verbose) << " Name=" << layer.layerName << " SpecVersion=" << layer.layerVersion; + mAvailableLayers[layer.layerName] = layer; + enumerateExtensions(layer.layerName, 4); + } + + setupExtensions(); + } + + OpenXRPlatform::~OpenXRPlatform() + { + } + +#if !XR_KHR_composition_layer_depth \ + || !XR_EXT_hp_mixed_reality_controller \ + || !XR_EXT_debug_utils \ + || !XR_HTC_vive_cosmos_controller_interaction \ + || !XR_HUAWEI_controller_interaction + +#error "OpenXR extensions missing. Please upgrade your copy of the OpenXR SDK to 1.0.13 minimum" +#endif + void OpenXRPlatform::setupExtensions() + { + std::vector optionalExtensions = { + XR_EXT_HP_MIXED_REALITY_CONTROLLER_EXTENSION_NAME, + XR_HTC_VIVE_COSMOS_CONTROLLER_INTERACTION_EXTENSION_NAME, + XR_HUAWEI_CONTROLLER_INTERACTION_EXTENSION_NAME + }; + + if (Settings::Manager::getBool("enable XR_KHR_composition_layer_depth", "VR Debug")) + optionalExtensions.emplace_back(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME); + + if (Settings::Manager::getBool("enable XR_EXT_debug_utils", "VR Debug")) + optionalExtensions.emplace_back(XR_EXT_DEBUG_UTILS_EXTENSION_NAME); + + selectGraphicsAPIExtension(); + + Log(Debug::Verbose) << "Using extensions:"; + + auto* graphicsAPIExtension = graphicsAPIExtensionName(); + if (!graphicsAPIExtension || !enableExtension(graphicsAPIExtension, true)) + { + throw std::runtime_error("No graphics APIs supported by openmw are supported by the OpenXR runtime."); + } + + for (auto optionalExtension : optionalExtensions) + enableExtension(optionalExtension, true); + } + + bool OpenXRPlatform::selectDirectX() + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + if (mPrivate->mWGL_NV_DX_interop2) + { + if (mAvailableExtensions.count(XR_KHR_D3D11_ENABLE_EXTENSION_NAME)) + { + mGraphicsAPIExtension = XR_KHR_D3D11_ENABLE_EXTENSION_NAME; + return true; + } + else + Log(Debug::Warning) << "Warning: Failed to select DirectX swapchains: OpenXR runtime does not support essential extension '" << XR_KHR_D3D11_ENABLE_EXTENSION_NAME << "'"; + } + else + Log(Debug::Warning) << "Warning: Failed to select DirectX swapchains: Essential WGL extension 'WGL_NV_DX_interop2' not supported by the graphics driver."; +#endif + return false; + } + + bool OpenXRPlatform::selectOpenGL() + { +#ifdef XR_USE_GRAPHICS_API_OPENGL + if (mAvailableExtensions.count(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME)) + { + mGraphicsAPIExtension = XR_KHR_OPENGL_ENABLE_EXTENSION_NAME; + return true; + } + else + Log(Debug::Warning) << "Warning: Failed to select OpenGL swapchains: OpenXR runtime does not support essential extension '" << XR_KHR_OPENGL_ENABLE_EXTENSION_NAME << "'"; +#endif + return false; + } + +#ifdef XR_USE_GRAPHICS_API_OPENGL +#if !XR_KHR_opengl_enable +#error "OpenXR extensions missing. Please upgrade your copy of the OpenXR SDK to 1.0.13 minimum" +#endif +#endif + +#ifdef XR_USE_GRAPHICS_API_D3D11 +#if !XR_KHR_D3D11_enable +#error "OpenXR extensions missing. Please upgrade your copy of the OpenXR SDK to 1.0.13 minimum" +#endif +#endif + + const char* OpenXRPlatform::graphicsAPIExtensionName() + { + return mGraphicsAPIExtension; + } + + void OpenXRPlatform::selectGraphicsAPIExtension() + { + bool preferDirectX = Settings::Manager::getBool("Prefer DirectX swapchains", "VR"); + + if (preferDirectX) + if (selectDirectX() || selectOpenGL()) + return; + if (selectOpenGL() || selectDirectX()) + return; + + Log(Debug::Verbose) << "Error: No graphics API supported by OpenMW VR is supported by the OpenXR runtime."; + throw std::runtime_error("Error: No graphics API supported by OpenMW VR is supported by the OpenXR runtime."); + } + + bool OpenXRPlatform::supportsExtension(const std::string& extensionName) const + { + return mAvailableExtensions.count(extensionName) > 0; + } + + bool OpenXRPlatform::supportsExtension(const std::string& extensionName, uint32_t minimumVersion) const + { + auto it = mAvailableExtensions.find(extensionName); + return it != mAvailableExtensions.end() && it->second.extensionVersion > minimumVersion; + } + + bool OpenXRPlatform::supportsLayer(const std::string& layerName) const + { + return mAvailableLayers.count(layerName) > 0; + } + + bool OpenXRPlatform::supportsLayer(const std::string& layerName, uint32_t minimumVersion) const + { + auto it = mAvailableLayers.find(layerName); + return it != mAvailableLayers.end() && it->second.layerVersion > minimumVersion; + } + + bool OpenXRPlatform::enableExtension(const std::string& extensionName, bool optional) + { + auto it = mAvailableExtensions.find(extensionName); + if (it != mAvailableExtensions.end()) + { + Log(Debug::Verbose) << " " << extensionName << ": enabled"; + mEnabledExtensions.push_back(it->second.extensionName); + return true; + } + else + { + Log(Debug::Verbose) << " " << extensionName << ": disabled (not supported)"; + if (!optional) + { + throw std::runtime_error(std::string("Required OpenXR extension ") + extensionName + " not supported by the runtime"); + } + return false; + } + } + + bool OpenXRPlatform::enableExtension(const std::string& extensionName, bool optional, uint32_t minimumVersion) + { + auto it = mAvailableExtensions.find(extensionName); + if (it != mAvailableExtensions.end() && it->second.extensionVersion > minimumVersion) + { + Log(Debug::Verbose) << " " << extensionName << ": enabled"; + mEnabledExtensions.push_back(it->second.extensionName); + return true; + } + else + { + Log(Debug::Verbose) << " " << extensionName << ": disabled (not supported)"; + if (!optional) + { + throw std::runtime_error(std::string("Required OpenXR extension ") + extensionName + " not supported by the runtime"); + } + return false; + } + } + bool OpenXRPlatform::extensionEnabled(const std::string& extensionName) const + { + for (auto* extension : mEnabledExtensions) + if (extension == extensionName) + return true; + return false; + } + XrInstance OpenXRPlatform::createXrInstance(const std::string& name) + { + XrInstance instance = XR_NULL_HANDLE; + XrInstanceCreateInfo createInfo{ XR_TYPE_INSTANCE_CREATE_INFO }; + createInfo.next = nullptr; + createInfo.enabledExtensionCount = mEnabledExtensions.size(); + createInfo.enabledExtensionNames = mEnabledExtensions.data(); + strcpy(createInfo.applicationInfo.applicationName, "openmw_vr"); + createInfo.applicationInfo.apiVersion = XR_CURRENT_API_VERSION; + + auto res = CHECK_XRCMD(xrCreateInstance(&createInfo, &instance)); + if (!XR_SUCCEEDED(res)) + initFailure(res, instance); + return instance; + } + + XrSession OpenXRPlatform::createXrSession(XrInstance instance, XrSystemId systemId) + { + XrSession session = XR_NULL_HANDLE; + XrResult res = XR_SUCCESS; +#ifdef _WIN32 + std::string graphicsAPIExtension = graphicsAPIExtensionName(); + if(graphicsAPIExtension == XR_KHR_OPENGL_ENABLE_EXTENSION_NAME) + { + // Get system requirements + PFN_xrGetOpenGLGraphicsRequirementsKHR p_getRequirements = nullptr; + CHECK_XRCMD(xrGetInstanceProcAddr(instance, "xrGetOpenGLGraphicsRequirementsKHR", reinterpret_cast(&p_getRequirements))); + XrGraphicsRequirementsOpenGLKHR requirements{ XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR }; + CHECK_XRCMD(p_getRequirements(instance, systemId, &requirements)); + + // TODO: Actually get system version + const XrVersion systemApiVersion = XR_MAKE_VERSION(4, 6, 0); + if (requirements.minApiVersionSupported > systemApiVersion) { + std::cout << "Runtime does not support desired Graphics API and/or version" << std::endl; + } + + // Create Session + auto DC = wglGetCurrentDC(); + auto GLRC = wglGetCurrentContext(); + + XrGraphicsBindingOpenGLWin32KHR graphicsBindings; + graphicsBindings.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR; + graphicsBindings.next = nullptr; + graphicsBindings.hDC = DC; + graphicsBindings.hGLRC = GLRC; + + if (!graphicsBindings.hDC) + Log(Debug::Warning) << "Missing DC"; + + XrSessionCreateInfo createInfo{ XR_TYPE_SESSION_CREATE_INFO }; + createInfo.next = &graphicsBindings; + createInfo.systemId = systemId; + res = CHECK_XRCMD(xrCreateSession(instance, &createInfo, &session)); + } +#ifdef XR_USE_GRAPHICS_API_D3D11 + else if(graphicsAPIExtension == XR_KHR_D3D11_ENABLE_EXTENSION_NAME) + { + PFN_xrGetD3D11GraphicsRequirementsKHR p_getRequirements = nullptr; + CHECK_XRCMD(xrGetInstanceProcAddr(instance, "xrGetD3D11GraphicsRequirementsKHR", reinterpret_cast(&p_getRequirements))); + XrGraphicsRequirementsD3D11KHR requirements{ XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR }; + CHECK_XRCMD(p_getRequirements(instance, systemId, &requirements)); + mPrivate->initializeD3D11(requirements); + + XrSessionCreateInfo createInfo{ XR_TYPE_SESSION_CREATE_INFO }; + createInfo.next = &mPrivate->mD3D11bindings; + createInfo.systemId = systemId; + res = CHECK_XRCMD(xrCreateSession(instance, &createInfo, &session)); + } +#endif + else + { + throw std::logic_error("Enum value not implemented"); + } +#elif __linux__ + { + // Get system requirements + PFN_xrGetOpenGLGraphicsRequirementsKHR p_getRequirements = nullptr; + xrGetInstanceProcAddr(instance, "xrGetOpenGLGraphicsRequirementsKHR", reinterpret_cast(&p_getRequirements)); + XrGraphicsRequirementsOpenGLKHR requirements{ XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR }; + CHECK_XRCMD(p_getRequirements(instance, systemId, &requirements)); + + // TODO: Actually get system version + const XrVersion systemApiVersion = XR_MAKE_VERSION(4, 6, 0); + if (requirements.minApiVersionSupported > systemApiVersion) { + std::cout << "Runtime does not support desired Graphics API and/or version" << std::endl; + } + + // Create Session + Display* xDisplay = XOpenDisplay(NULL); + GLXContext glxContext = glXGetCurrentContext(); + GLXDrawable glxDrawable = glXGetCurrentDrawable(); + + // TODO: runtimes don't actually care (yet) + GLXFBConfig glxFBConfig = 0; + uint32_t visualid = 0; + + XrGraphicsBindingOpenGLXlibKHR graphicsBindings; + graphicsBindings.type = XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR; + graphicsBindings.next = nullptr; + graphicsBindings.xDisplay = xDisplay; + graphicsBindings.glxContext = glxContext; + graphicsBindings.glxDrawable = glxDrawable; + graphicsBindings.glxFBConfig = glxFBConfig; + graphicsBindings.visualid = visualid; + + if (!graphicsBindings.glxContext) + Log(Debug::Warning) << "Missing glxContext"; + + if (!graphicsBindings.glxDrawable) + Log(Debug::Warning) << "Missing glxDrawable"; + + XrSessionCreateInfo createInfo{ XR_TYPE_SESSION_CREATE_INFO }; + createInfo.next = &graphicsBindings; + createInfo.systemId = systemId; + res = CHECK_XRCMD(xrCreateSession(instance, &createInfo, &session)); + } +#endif + if (!XR_SUCCEEDED(res)) + initFailure(res, instance); + + uint32_t swapchainFormatCount; + CHECK_XRCMD(xrEnumerateSwapchainFormats(session, 0, &swapchainFormatCount, nullptr)); + mSwapchainFormats.resize(swapchainFormatCount); + CHECK_XRCMD(xrEnumerateSwapchainFormats(session, (uint32_t)mSwapchainFormats.size(), &swapchainFormatCount, mSwapchainFormats.data())); + + std::stringstream ss; + ss << "Available Swapchain formats: (" << swapchainFormatCount << ")" << std::endl; + + for (auto format : mSwapchainFormats) + { + ss << " Enum=" << std::dec << format << " (0x=" << std::hex << format << ")" << std::dec << std::endl; + } + + Log(Debug::Verbose) << ss.str(); + + return session; + } + + /* + * For reference: These are the DXGI formats offered by WMR when using D3D11: + Enum=29 //(0x=1d) DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + Enum=91 //(0x=5b) DXGI_FORMAT_B8G8R8A8_UNORM_SRGB + Enum=28 //(0x=1c) DXGI_FORMAT_R8G8B8A8_UNORM + Enum=87 //(0x=57) DXGI_FORMAT_B8G8R8A8_UNORM + Enum=40 //(0x=28) DXGI_FORMAT_D32_FLOAT + Enum=20 //(0x=14) DXGI_FORMAT_D32_FLOAT_S8X24_UINT + Enum=45 //(0x=2d) DXGI_FORMAT_D24_UNORM_S8_UINT + Enum=55 //(0x=37) DXGI_FORMAT_D16_UNORM + * And these extra formats are offered by SteamVR: + 0xa , // DXGI_FORMAT_R16G16B16A16_FLOAT + 0x18 , // DXGI_FORMAT_R10G10B10A2_UNORM + */ + + int64_t OpenXRPlatform::selectColorFormat() + { + std::string graphicsAPIExtension = graphicsAPIExtensionName(); + if (graphicsAPIExtension == XR_KHR_OPENGL_ENABLE_EXTENSION_NAME) + { + std::vector requestedColorSwapchainFormats; + + if (Settings::Manager::getBool("Prefer sRGB swapchains", "VR")) + { + requestedColorSwapchainFormats.push_back(0x8C43); // GL_SRGB8_ALPHA8 + requestedColorSwapchainFormats.push_back(0x8C41); // GL_SRGB8 + } + + requestedColorSwapchainFormats.push_back(0x8058); // GL_RGBA8 + requestedColorSwapchainFormats.push_back(0x8F97); // GL_RGBA8_SNORM + requestedColorSwapchainFormats.push_back(0x881A); // GL_RGBA16F + requestedColorSwapchainFormats.push_back(0x881B); // GL_RGB16F + requestedColorSwapchainFormats.push_back(0x8C3A); // GL_R11F_G11F_B10F + + return selectFormat(requestedColorSwapchainFormats); + } +#ifdef XR_USE_GRAPHICS_API_D3D11 + else if (graphicsAPIExtension == XR_KHR_D3D11_ENABLE_EXTENSION_NAME) + { + std::vector requestedColorSwapchainFormats; + + if (Settings::Manager::getBool("Prefer sRGB swapchains", "VR")) + { + requestedColorSwapchainFormats.push_back(0x1d); // DXGI_FORMAT_R8G8B8A8_UNORM_SRGB + requestedColorSwapchainFormats.push_back(0x5b); // DXGI_FORMAT_B8G8R8A8_UNORM_SRGB + } + + requestedColorSwapchainFormats.push_back(0x1c); // DXGI_FORMAT_R8G8B8A8_UNORM + requestedColorSwapchainFormats.push_back(0x57); // DXGI_FORMAT_B8G8R8A8_UNORM + requestedColorSwapchainFormats.push_back(0xa); // DXGI_FORMAT_R16G16B16A16_FLOAT + requestedColorSwapchainFormats.push_back(0x18); // DXGI_FORMAT_R10G10B10A2_UNORM + + return selectFormat(requestedColorSwapchainFormats); + } +#endif + else + { + throw std::logic_error("Enum value not implemented"); + } + + } + + int64_t OpenXRPlatform::selectDepthFormat() + { + std::string graphicsAPIExtension = graphicsAPIExtensionName(); + if (graphicsAPIExtension == XR_KHR_OPENGL_ENABLE_EXTENSION_NAME) + { + // Find supported depth swapchain format. + std::vector requestedDepthSwapchainFormats = { + 0x88F0, // GL_DEPTH24_STENCIL8 + 0x8CAC, // GL_DEPTH_COMPONENT32F + 0x81A7, // GL_DEPTH_COMPONENT32 + 0x8DAB, // GL_DEPTH_COMPONENT32F_NV + 0x8CAD, // GL_DEPTH32_STENCIL8 + 0x81A6, // GL_DEPTH_COMPONENT24 + // Need 32bit minimum: // 0x81A5, // GL_DEPTH_COMPONENT16 + }; + + return selectFormat(requestedDepthSwapchainFormats); + } +#ifdef XR_USE_GRAPHICS_API_D3D11 + else if (graphicsAPIExtension == XR_KHR_D3D11_ENABLE_EXTENSION_NAME) + { + // Find supported color swapchain format. + std::vector requestedDepthSwapchainFormats = { + 0x2d, // DXGI_FORMAT_D24_UNORM_S8_UINT + 0x14, // DXGI_FORMAT_D32_FLOAT_S8X24_UINT + 0x28, // DXGI_FORMAT_D32_FLOAT + // Need 32bit minimum: 0x37, // DXGI_FORMAT_D16_UNORM + }; + return selectFormat(requestedDepthSwapchainFormats); + } +#endif + else + { + throw std::logic_error("Enum value not implemented"); + } + } + + void OpenXRPlatform::eraseFormat(int64_t format) + { + for (auto it = mSwapchainFormats.begin(); it != mSwapchainFormats.end(); it++) + { + if (*it == format) + { + mSwapchainFormats.erase(it); + return; + } + } + } + + int64_t OpenXRPlatform::selectFormat(const std::vector& requestedFormats) + { + auto it = + std::find_first_of(std::begin(requestedFormats), std::end(requestedFormats), + mSwapchainFormats.begin(), mSwapchainFormats.end()); + if (it == std::end(requestedFormats)) + { + return 0; + } + return *it; + } + void* OpenXRPlatform::DXRegisterObject(void* dxResource, uint32_t glName, uint32_t glType, bool discard, void* ntShareHandle) + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + if (ntShareHandle) + { + mPrivate->wglDXSetResourceShareHandleNV(dxResource, ntShareHandle); + } + return mPrivate->wglDXRegisterObjectNV(mPrivate->wglDXDevice, dxResource, glName, glType, 1); +#else + return nullptr; +#endif + } + void OpenXRPlatform::DXUnregisterObject(void* dxResourceShareHandle) + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + mPrivate->wglDXUnregisterObjectNV(mPrivate->wglDXDevice, dxResourceShareHandle); +#endif + } + void OpenXRPlatform::DXLockObject(void* dxResourceShareHandle) + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + mPrivate->wglDXLockObjectsNV(mPrivate->wglDXDevice, 1, &dxResourceShareHandle); +#endif + } + void OpenXRPlatform::DXUnlockObject(void* dxResourceShareHandle) + { +#ifdef XR_USE_GRAPHICS_API_D3D11 + mPrivate->wglDXUnlockObjectsNV(mPrivate->wglDXDevice, 1, &dxResourceShareHandle); +#endif + } + + static XrInstanceProperties + getInstanceProperties(XrInstance instance) + { + XrInstanceProperties properties{ XR_TYPE_INSTANCE_PROPERTIES }; + if (instance) + xrGetInstanceProperties(instance, &properties); + return properties; + } + + std::string OpenXRPlatform::getInstanceName(XrInstance instance) + { + if (instance) + return getInstanceProperties(instance).runtimeName; + return "unknown"; + } + + XrVersion OpenXRPlatform::getInstanceVersion(XrInstance instance) + { + if (instance) + return getInstanceProperties(instance).runtimeVersion; + return 0; + } + void OpenXRPlatform::initFailure(XrResult res, XrInstance instance) + { + std::stringstream ss; + std::string runtimeName = getInstanceName(instance); + XrVersion runtimeVersion = getInstanceVersion(instance); + ss << "Error caught while initializing VR device: " << XrResultString(res) << std::endl; + ss << "Device: " << runtimeName << std::endl; + ss << "Version: " << runtimeVersion << std::endl; + if (res == XR_ERROR_FORM_FACTOR_UNAVAILABLE) + { + ss << "Cause: Unable to open VR device. Make sure your device is plugged in and the VR driver is running." << std::endl; + ss << std::endl; + if (runtimeName == "Oculus" || runtimeName == "Quest") + { + ss << "Your device has been identified as an Oculus device." << std::endl; + ss << "The most common cause for this error when using an oculus device, is quest users attempting to run the game via Virtual Desktop." << std::endl; + ss << "Unfortunately this is currently broken, and quest users will need to play via a link cable." << std::endl; + } + } + else if (res == XR_ERROR_LIMIT_REACHED) + { + ss << "Cause: Device resources exhausted. Close other VR applications if you have any open. If you have none, you may need to reboot to reset the driver." << std::endl; + } + else + { + ss << "Cause: Unknown. Make sure your device is plugged in and ready." << std::endl; + } + throw std::runtime_error(ss.str()); + } +} diff --git a/apps/openmw/mwvr/openxrplatform.hpp b/apps/openmw/mwvr/openxrplatform.hpp new file mode 100644 index 000000000..f0d623bf2 --- /dev/null +++ b/apps/openmw/mwvr/openxrplatform.hpp @@ -0,0 +1,86 @@ +#ifndef OPENXR_PLATFORM_HPP +#define OPENXR_PLATFORM_HPP + +#include +#include +#include +#include + +#include + +namespace MWVR +{ + // 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); + + struct OpenXRPlatformPrivate; + + class OpenXRPlatform + { + public: + using ExtensionMap = std::map; + using LayerMap = std::map; + using LayerExtensionMap = std::map; + + public: + OpenXRPlatform(osg::GraphicsContext* gc); + ~OpenXRPlatform(); + + const char* graphicsAPIExtensionName(); + bool supportsExtension(const std::string& extensionName) const; + bool supportsExtension(const std::string& extensionName, uint32_t minimumVersion) const; + bool supportsLayer(const std::string& layerName) const; + bool supportsLayer(const std::string& layerName, uint32_t minimumVersion) const; + + bool enableExtension(const std::string& extensionName, bool optional); + bool enableExtension(const std::string& extensionName, bool optional, uint32_t minimumVersion); + + bool extensionEnabled(const std::string& extensionName) const; + + XrInstance createXrInstance(const std::string& name); + XrSession createXrSession(XrInstance instance, XrSystemId systemId); + + int64_t selectColorFormat(); + int64_t selectDepthFormat(); + int64_t selectFormat(const std::vector& requestedFormats); + void eraseFormat(int64_t format); + std::vector mSwapchainFormats{}; + + /// Registers an object for sharing as if calling wglDXRegisterObjectNV requesting write access. + /// If ntShareHandle is not null, wglDXSetResourceShareHandleNV is called first to register the share handle + void* DXRegisterObject(void* dxResource, uint32_t glName, uint32_t glType, bool discard, void* ntShareHandle); + /// Unregisters an object from sharing as if calling wglDXUnregisterObjectNV + void DXUnregisterObject(void* dxResourceShareHandle); + /// Locks a DX object for use by OpenGL as if calling wglDXLockObjectsNV + void DXLockObject(void* dxResourceShareHandle); + /// Unlocks a DX object for use by DirectX as if calling wglDXUnlockObjectsNV + void DXUnlockObject(void* dxResourceShareHandle); + + std::string getInstanceName(XrInstance instance); + XrVersion getInstanceVersion(XrInstance instance); + void initFailure(XrResult, XrInstance); + + private: + void enumerateExtensions(const char* layerName, int logIndent); + void setupExtensions(); + void selectGraphicsAPIExtension(); + bool selectDirectX(); + bool selectOpenGL(); + + ExtensionMap mAvailableExtensions; + LayerMap mAvailableLayers; + LayerExtensionMap mAvailableLayerExtensions; + std::vector mEnabledExtensions; + const char* mGraphicsAPIExtension = nullptr; + + std::unique_ptr< OpenXRPlatformPrivate > mPrivate; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrswapchain.cpp b/apps/openmw/mwvr/openxrswapchain.cpp new file mode 100644 index 000000000..e7ddca577 --- /dev/null +++ b/apps/openmw/mwvr/openxrswapchain.cpp @@ -0,0 +1,48 @@ +#include "openxrswapchain.hpp" +#include "openxrswapchainimpl.hpp" +#include "openxrmanager.hpp" +#include "openxrmanagerimpl.hpp" +#include "vrenvironment.hpp" + +#include + +namespace MWVR { + OpenXRSwapchain::OpenXRSwapchain(osg::ref_ptr state, SwapchainConfig config) + : mPrivate(new OpenXRSwapchainImpl(state, config)) + { + } + + OpenXRSwapchain::~OpenXRSwapchain() + { + } + + void OpenXRSwapchain::beginFrame(osg::GraphicsContext* gc) + { + return impl().beginFrame(gc); + } + + void OpenXRSwapchain::endFrame(osg::GraphicsContext* gc, VRFramebuffer& readBuffer) + { + return impl().endFrame(gc, readBuffer); + } + + int OpenXRSwapchain::width() const + { + return impl().width(); + } + + int OpenXRSwapchain::height() const + { + return impl().height(); + } + + int OpenXRSwapchain::samples() const + { + return impl().samples(); + } + + bool OpenXRSwapchain::isAcquired() const + { + return impl().isAcquired(); + } +} diff --git a/apps/openmw/mwvr/openxrswapchain.hpp b/apps/openmw/mwvr/openxrswapchain.hpp new file mode 100644 index 000000000..168fa81d4 --- /dev/null +++ b/apps/openmw/mwvr/openxrswapchain.hpp @@ -0,0 +1,53 @@ +#ifndef OPENXR_SWAPCHAIN_HPP +#define OPENXR_SWAPCHAIN_HPP + +#include "openxrmanager.hpp" + +struct XrSwapchainSubImage; + +namespace MWVR +{ + class OpenXRSwapchainImpl; + class VRFramebuffer; + + /// \brief Creation and management of openxr swapchains + class OpenXRSwapchain + { + public: + OpenXRSwapchain(osg::ref_ptr state, SwapchainConfig config); + ~OpenXRSwapchain(); + + public: + //! Prepare for render (set FBO) + void beginFrame(osg::GraphicsContext* gc); + + //! Finalize render + void endFrame(osg::GraphicsContext* gc, VRFramebuffer& readBuffer); + + //! Whether subchain is currently acquired (true) or released (false) + bool isAcquired() const; + + //! Width of the view surface + int width() const; + + //! Height of the view surface + int height() const; + + //! Samples of the view surface + int samples() const; + + //! Get the private implementation + OpenXRSwapchainImpl& impl() { return *mPrivate; } + + //! Get the private implementation + const OpenXRSwapchainImpl& impl() const { return *mPrivate; } + + protected: + OpenXRSwapchain(const OpenXRSwapchain&) = delete; + void operator=(const OpenXRSwapchain&) = delete; + private: + std::unique_ptr mPrivate; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrswapchainimage.cpp b/apps/openmw/mwvr/openxrswapchainimage.cpp new file mode 100644 index 000000000..e7a309808 --- /dev/null +++ b/apps/openmw/mwvr/openxrswapchainimage.cpp @@ -0,0 +1,221 @@ +#include "openxrswapchainimage.hpp" +#include "openxrmanagerimpl.hpp" +#include "vrenvironment.hpp" +#include "vrframebuffer.hpp" + +// The OpenXR SDK's platform headers assume we've included platform headers +#ifdef _WIN32 +#include +#include + +#ifdef XR_USE_GRAPHICS_API_D3D11 +#include +#include +#endif + +#elif __linux__ +#include +#include +#undef None + +#else +#error Unsupported platform +#endif + +#include +#include +#include +#include + +#include + +#define GLERR if(auto err = glGetError() != GL_NO_ERROR) Log(Debug::Verbose) << __FILE__ << "." << __LINE__ << ": " << glGetError() + +namespace MWVR { + + template + class OpenXRSwapchainImageTemplate; + + template<> + class OpenXRSwapchainImageTemplate< XrSwapchainImageOpenGLKHR > : public OpenXRSwapchainImage + { + public: + static constexpr XrStructureType XrType = XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR; + + public: + OpenXRSwapchainImageTemplate(osg::GraphicsContext* gc, XrSwapchainCreateInfo swapchainCreateInfo, const XrSwapchainImageOpenGLKHR& xrImage) + : OpenXRSwapchainImage() + , mXrImage(xrImage) + , mBufferBits(0) + , mFramebuffer(nullptr) + { + mFramebuffer.reset(new VRFramebuffer(gc->getState(), swapchainCreateInfo.width, swapchainCreateInfo.height, swapchainCreateInfo.sampleCount)); + if (swapchainCreateInfo.usageFlags & XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) + { + mFramebuffer->setDepthBuffer(gc, mXrImage.image, false); + mBufferBits = GL_DEPTH_BUFFER_BIT; + } + else + { + mFramebuffer->setColorBuffer(gc, mXrImage.image, false); + mBufferBits = GL_COLOR_BUFFER_BIT; + } + } + + void blit(osg::GraphicsContext* gc, VRFramebuffer& readBuffer, int offset_x, int offset_y) override + { + mFramebuffer->bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + readBuffer.blit(gc, offset_x, offset_y, offset_x + mFramebuffer->width(), offset_y + mFramebuffer->height(), 0, 0, mFramebuffer->width(), mFramebuffer->height(), mBufferBits, GL_NEAREST); + } + + XrSwapchainImageOpenGLKHR mXrImage; + uint32_t mBufferBits; + std::unique_ptr mFramebuffer; + }; + +#ifdef XR_USE_GRAPHICS_API_D3D11 + template<> + class OpenXRSwapchainImageTemplate< XrSwapchainImageD3D11KHR > : public OpenXRSwapchainImage + { + public: + static constexpr XrStructureType XrType = XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR; + public: + OpenXRSwapchainImageTemplate(osg::GraphicsContext* gc, XrSwapchainCreateInfo swapchainCreateInfo, const XrSwapchainImageD3D11KHR& xrImage) + : OpenXRSwapchainImage() + , mXrImage(xrImage) + , mBufferBits(0) + , mFramebuffer(nullptr) + { + mXrImage.texture->GetDevice(&mDevice); + mDevice->GetImmediateContext(&mDeviceContext); + + mXrImage.texture->GetDesc(&mDesc); + + glGenTextures(1, &mGlTextureName); + + auto* xr = Environment::get().getManager(); + //mDxResourceShareHandle = xr->impl().platform().DXRegisterObject(mXrImage.texture, mGlTextureName, GL_TEXTURE_2D, true, nullptr); + + if (!mDxResourceShareHandle) + { + // Some OpenXR runtimes return textures that cannot be directly shared. + // So we need to make a redundant texture to use as an intermediary... + mSharedTextureDesc.Width = mDesc.Width; + mSharedTextureDesc.Height = mDesc.Height; + mSharedTextureDesc.MipLevels = mDesc.MipLevels; + mSharedTextureDesc.ArraySize = mDesc.ArraySize; + mSharedTextureDesc.Format = static_cast(swapchainCreateInfo.format); + mSharedTextureDesc.SampleDesc = mDesc.SampleDesc; + mSharedTextureDesc.Usage = D3D11_USAGE_DEFAULT; + mSharedTextureDesc.BindFlags = 0; + mSharedTextureDesc.CPUAccessFlags = 0; + mSharedTextureDesc.MiscFlags = 0;; + + mDevice->CreateTexture2D(&mSharedTextureDesc, nullptr, &mSharedTexture); + mXrImage.texture->GetDesc(&mSharedTextureDesc); + mDxResourceShareHandle = xr->impl().platform().DXRegisterObject(mSharedTexture, mGlTextureName, GL_TEXTURE_2D, true, nullptr); + } + + // Set up shared texture as blit target + mFramebuffer.reset(new VRFramebuffer(gc->getState(), swapchainCreateInfo.width, swapchainCreateInfo.height, swapchainCreateInfo.sampleCount)); + + if (swapchainCreateInfo.usageFlags & XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) + { + mFramebuffer->setDepthBuffer(gc, mGlTextureName, false); + mBufferBits = GL_DEPTH_BUFFER_BIT; + } + else + { + mFramebuffer->setColorBuffer(gc, mGlTextureName, false); + mBufferBits = GL_COLOR_BUFFER_BIT; + } + } + + ~OpenXRSwapchainImageTemplate() + { + auto* xr = Environment::get().getManager(); + if (mDxResourceShareHandle) + xr->impl().platform().DXUnregisterObject(mDxResourceShareHandle); + glDeleteTextures(1, &mGlTextureName); + } + + void blit(osg::GraphicsContext* gc, VRFramebuffer& readBuffer, int offset_x, int offset_y) override + { + // Blit readBuffer into directx texture, while flipping the Y axis. + auto* xr = Environment::get().getManager(); + xr->impl().platform().DXLockObject(mDxResourceShareHandle); + mFramebuffer->bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + readBuffer.blit(gc, offset_x, offset_y, offset_x + mFramebuffer->width(), offset_y + mFramebuffer->height(), 0, mFramebuffer->height(), mFramebuffer->width(), 0, mBufferBits, GL_NEAREST); + xr->impl().platform().DXUnlockObject(mDxResourceShareHandle); + + + // If the d3d11 texture couldn't be shared directly, blit it again. + if (mSharedTexture) + { + mDeviceContext->CopyResource(mXrImage.texture, mSharedTexture); + } + } + + ID3D11Device* mDevice = nullptr; + ID3D11DeviceContext* mDeviceContext = nullptr; + D3D11_TEXTURE2D_DESC mDesc; + D3D11_TEXTURE2D_DESC mSharedTextureDesc; + ID3D11Texture2D* mSharedTexture = nullptr; + uint32_t mGlTextureName = 0; + void* mDxResourceShareHandle = nullptr; + + XrSwapchainImageD3D11KHR mXrImage; + uint32_t mBufferBits; + std::unique_ptr mFramebuffer; + }; +#endif + + + template< typename Image > static inline + std::vector > + enumerateSwapchainImagesImpl(osg::GraphicsContext* gc, XrSwapchain swapchain, XrSwapchainCreateInfo swapchainCreateInfo) + { + using SwapchainImage = OpenXRSwapchainImageTemplate; + + uint32_t imageCount = 0; + std::vector< Image > images; + CHECK_XRCMD(xrEnumerateSwapchainImages(swapchain, 0, &imageCount, nullptr)); + images.resize(imageCount, { SwapchainImage::XrType }); + CHECK_XRCMD(xrEnumerateSwapchainImages(swapchain, imageCount, &imageCount, reinterpret_cast(images.data()))); + + std::vector > swapchainImages; + for(auto& image: images) + { + swapchainImages.emplace_back(new OpenXRSwapchainImageTemplate(gc, swapchainCreateInfo, image)); + } + + return swapchainImages; + } + + std::vector > + OpenXRSwapchainImage::enumerateSwapchainImages(osg::GraphicsContext* gc, XrSwapchain swapchain, XrSwapchainCreateInfo swapchainCreateInfo) + { + auto* xr = Environment::get().getManager(); + + if (xr->xrExtensionIsEnabled(XR_KHR_OPENGL_ENABLE_EXTENSION_NAME)) + { + return enumerateSwapchainImagesImpl(gc, swapchain, swapchainCreateInfo); + } +#ifdef XR_USE_GRAPHICS_API_D3D11 + else if (xr->xrExtensionIsEnabled(XR_KHR_D3D11_ENABLE_EXTENSION_NAME)) + { + return enumerateSwapchainImagesImpl(gc, swapchain, swapchainCreateInfo); + } +#endif + else + { + throw std::logic_error("Implementation missing for selected graphics API"); + } + + return std::vector>(); + } + + OpenXRSwapchainImage::OpenXRSwapchainImage() + { + } +} diff --git a/apps/openmw/mwvr/openxrswapchainimage.hpp b/apps/openmw/mwvr/openxrswapchainimage.hpp new file mode 100644 index 000000000..1837317ca --- /dev/null +++ b/apps/openmw/mwvr/openxrswapchainimage.hpp @@ -0,0 +1,27 @@ +#ifndef OPENXR_SWAPCHAINIMAGE_HPP +#define OPENXR_SWAPCHAINIMAGE_HPP + +#include +#include + +#include +#include + +#include "vrframebuffer.hpp" + +namespace MWVR +{ + class OpenXRSwapchainImage + { + public: + static std::vector< std::unique_ptr > + enumerateSwapchainImages(osg::GraphicsContext* gc, XrSwapchain swapchain, XrSwapchainCreateInfo swapchainCreateInfo); + + OpenXRSwapchainImage(); + virtual ~OpenXRSwapchainImage() {}; + + virtual void blit(osg::GraphicsContext* gc, VRFramebuffer& readBuffer, int offset_x, int offset_y) = 0; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrswapchainimpl.cpp b/apps/openmw/mwvr/openxrswapchainimpl.cpp new file mode 100644 index 000000000..b223365c9 --- /dev/null +++ b/apps/openmw/mwvr/openxrswapchainimpl.cpp @@ -0,0 +1,263 @@ +#include "openxrswapchainimpl.hpp" +#include "openxrdebug.hpp" +#include "vrenvironment.hpp" +#include "vrframebuffer.hpp" + +#include + +namespace MWVR { + OpenXRSwapchainImpl::OpenXRSwapchainImpl(osg::ref_ptr state, SwapchainConfig config) + : mConfig(config) + { + if (mConfig.selectedWidth <= 0) + throw std::invalid_argument("Width must be a positive integer"); + if (mConfig.selectedHeight <= 0) + throw std::invalid_argument("Height must be a positive integer"); + if (mConfig.selectedSamples <= 0) + throw std::invalid_argument("Samples must be a positive integer"); + + mSwapchain.reset(new SwapchainPrivate(state, mConfig, SwapchainPrivate::Use::COLOR)); + mConfig.selectedSamples = mSwapchain->samples(); + + auto* xr = Environment::get().getManager(); + + if (xr->xrExtensionIsEnabled(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME)) + { + try + { + mSwapchainDepth.reset(new SwapchainPrivate(state, mConfig, SwapchainPrivate::Use::DEPTH)); + } + catch (std::exception& e) + { + Log(Debug::Warning) << "XR_KHR_composition_layer_depth was enabled but creating depth swapchain failed: " << e.what(); + mSwapchainDepth = nullptr; + } + } + } + + OpenXRSwapchainImpl::~OpenXRSwapchainImpl() + { + } + + bool OpenXRSwapchainImpl::isAcquired() const + { + return mFormallyAcquired; + } + + void OpenXRSwapchainImpl::beginFrame(osg::GraphicsContext* gc) + { + acquire(gc); + } + + int swapCount = 0; + + void OpenXRSwapchainImpl::endFrame(osg::GraphicsContext* gc, VRFramebuffer& readBuffer) + { + checkAcquired(); + release(gc, readBuffer); + } + + void OpenXRSwapchainImpl::acquire(osg::GraphicsContext* gc) + { + if (isAcquired()) + throw std::logic_error("Trying to acquire already acquired swapchain"); + + // The openxr runtime may fail to acquire/release. + // Do not re-acquire a swapchain before having successfully released it. + // Lest the swapchain fall out of sync. + if (!mShouldRelease) + { + mSwapchain->acquire(gc); + mShouldRelease = mSwapchain->isAcquired(); + if (mSwapchainDepth && mSwapchain->isAcquired()) + { + mSwapchainDepth->acquire(gc); + mShouldRelease = mSwapchainDepth->isAcquired(); + } + } + + mFormallyAcquired = true; + } + + void OpenXRSwapchainImpl::release(osg::GraphicsContext* gc, VRFramebuffer& readBuffer) + { + // The openxr runtime may fail to acquire/release. + // Do not release a swapchain before having successfully acquire it. + if (mShouldRelease) + { + mSwapchain->blitAndRelease(gc, readBuffer); + mShouldRelease = mSwapchain->isAcquired(); + if (mSwapchainDepth) + { + mSwapchainDepth->blitAndRelease(gc, readBuffer); + mShouldRelease = mSwapchainDepth->isAcquired(); + } + } + + mFormallyAcquired = false; + } + + void OpenXRSwapchainImpl::checkAcquired() const + { + if (!isAcquired()) + throw std::logic_error("Swapchain must be acquired before use. Call between OpenXRSwapchain::beginFrame() and OpenXRSwapchain::endFrame()"); + } + + OpenXRSwapchainImpl::SwapchainPrivate::SwapchainPrivate(osg::ref_ptr state, SwapchainConfig config, Use use) + : mConfig(config) + , mImages() + , mWidth(config.selectedWidth) + , mHeight(config.selectedHeight) + , mSamples(1) + , mUsage(use) + { + auto* xr = Environment::get().getManager(); + + XrSwapchainCreateInfo swapchainCreateInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO }; + swapchainCreateInfo.arraySize = 1; + swapchainCreateInfo.width = mWidth; + swapchainCreateInfo.height = mHeight; + swapchainCreateInfo.mipCount = 1; + swapchainCreateInfo.faceCount = 1; + + while (mSamples > 0 && mSwapchain == XR_NULL_HANDLE && mFormat == 0) + { + // Select a swapchain format. + if (use == Use::COLOR) + mFormat = xr->selectColorFormat(); + else + mFormat = xr->selectDepthFormat(); + std::string typeString = use == Use::COLOR ? "color" : "depth"; + if (mFormat == 0) { + throw std::runtime_error(std::string("Swapchain ") + typeString + " format not supported"); + } + Log(Debug::Verbose) << "Selected " << typeString << " format: " << std::dec << mFormat << " (" << std::hex << mFormat << ")" << std::dec; + + // Now create the swapchain + Log(Debug::Verbose) << "Creating swapchain with dimensions Width=" << mWidth << " Heigh=" << mHeight << " SampleCount=" << mSamples; + swapchainCreateInfo.format = mFormat; + swapchainCreateInfo.sampleCount = mSamples; + if(mUsage == Use::COLOR) + swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; + else + swapchainCreateInfo.usageFlags = XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; + auto res = xrCreateSwapchain(xr->impl().xrSession(), &swapchainCreateInfo, &mSwapchain); + + // Check errors and try again if possible + if (res == XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED) + { + // We only try swapchain formats enumerated by the runtime itself. + // This does not guarantee that that swapchain format is going to be supported for this specific usage. + Log(Debug::Verbose) << "Failed to create swapchain with Format=" << mFormat<< ": " << XrResultString(res); + xr->eraseFormat(mFormat); + mFormat = 0; + continue; + } + else if (!XR_SUCCEEDED(res)) + { + Log(Debug::Verbose) << "Failed to create swapchain with SampleCount=" << mSamples << ": " << XrResultString(res); + mSamples /= 2; + if (mSamples == 0) + { + CHECK_XRRESULT(res, "xrCreateSwapchain"); + throw std::runtime_error(XrResultString(res)); + } + continue; + } + + CHECK_XRRESULT(res, "xrCreateSwapchain"); + VrDebug::setName(mSwapchain, "OpenMW XR Color Swapchain " + config.name); + } + + // TODO: here + mImages = OpenXRSwapchainImage::enumerateSwapchainImages(state->getGraphicsContext(), mSwapchain, swapchainCreateInfo); + mSubImage.swapchain = mSwapchain; + mSubImage.imageRect.offset = { 0, 0 }; + mSubImage.imageRect.extent = { mWidth, mHeight }; + } + + OpenXRSwapchainImpl::SwapchainPrivate::~SwapchainPrivate() + { + if (mSwapchain) + CHECK_XRCMD(xrDestroySwapchain(mSwapchain)); + } + + uint32_t OpenXRSwapchainImpl::SwapchainPrivate::count() const + { + return mImages.size(); + } + + bool OpenXRSwapchainImpl::SwapchainPrivate::isAcquired() const + { + return mIsReady; + } + + void OpenXRSwapchainImpl::SwapchainPrivate::acquire(osg::GraphicsContext* gc) + { + auto xr = Environment::get().getManager(); + XrSwapchainImageAcquireInfo acquireInfo{ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; + XrSwapchainImageWaitInfo waitInfo{ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO }; + waitInfo.timeout = XR_INFINITE_DURATION; + if (!mIsIndexAcquired) + { + mIsIndexAcquired = XR_SUCCEEDED(CHECK_XRCMD(xrAcquireSwapchainImage(mSwapchain, &acquireInfo, &mAcquiredIndex))); + if (mIsIndexAcquired) + xr->xrResourceAcquired(); + } + if (mIsIndexAcquired && !mIsReady) + { + mIsReady = XR_SUCCEEDED(CHECK_XRCMD(xrWaitSwapchainImage(mSwapchain, &waitInfo))); + } + } + void OpenXRSwapchainImpl::SwapchainPrivate::blitAndRelease(osg::GraphicsContext* gc, VRFramebuffer& readBuffer) + { + auto xr = Environment::get().getManager(); + + XrSwapchainImageReleaseInfo releaseInfo{ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + if (mIsReady) + { + mImages[mAcquiredIndex]->blit(gc, readBuffer, mConfig.offsetWidth, mConfig.offsetHeight); + + mIsReady = !XR_SUCCEEDED(CHECK_XRCMD(xrReleaseSwapchainImage(mSwapchain, &releaseInfo))); + if (!mIsReady) + { + mIsIndexAcquired = false; + xr->xrResourceReleased(); + } + } + } + + void OpenXRSwapchainImpl::SwapchainPrivate::checkAcquired() const + { + if (!isAcquired()) + throw std::logic_error("Swapchain must be acquired before use. Call between OpenXRSwapchain::beginFrame() and OpenXRSwapchain::endFrame()"); + } + + XrSwapchain OpenXRSwapchainImpl::xrSwapchain(void) const + { + if(mSwapchain) + return mSwapchain->xrSwapchain(); + return XR_NULL_HANDLE; + } + + XrSwapchain OpenXRSwapchainImpl::xrSwapchainDepth(void) const + { + if (mSwapchainDepth) + return mSwapchainDepth->xrSwapchain(); + return XR_NULL_HANDLE; + } + + XrSwapchainSubImage OpenXRSwapchainImpl::xrSubImage(void) const + { + if (mSwapchain) + return mSwapchain->xrSubImage(); + return XrSwapchainSubImage{ XR_NULL_HANDLE }; + } + + XrSwapchainSubImage OpenXRSwapchainImpl::xrSubImageDepth(void) const + { + if (mSwapchainDepth) + return mSwapchainDepth->xrSubImage(); + return XrSwapchainSubImage{ XR_NULL_HANDLE }; + } +} diff --git a/apps/openmw/mwvr/openxrswapchainimpl.hpp b/apps/openmw/mwvr/openxrswapchainimpl.hpp new file mode 100644 index 000000000..b850e95c8 --- /dev/null +++ b/apps/openmw/mwvr/openxrswapchainimpl.hpp @@ -0,0 +1,90 @@ +#ifndef OPENXR_SWAPCHAINIMPL_HPP +#define OPENXR_SWAPCHAINIMPL_HPP + +#include "openxrswapchainimage.hpp" +#include "openxrmanagerimpl.hpp" + +struct XrSwapchainSubImage; + +namespace MWVR +{ + class VRFramebuffer; + + /// \brief Implementation of OpenXRSwapchain + class OpenXRSwapchainImpl + { + private: + struct SwapchainPrivate + { + enum class Use { + COLOR = 0, + DEPTH = 1 + }; + + SwapchainPrivate(osg::ref_ptr state, SwapchainConfig config, Use use); + ~SwapchainPrivate(); + + uint32_t count() const; + bool isAcquired() const; + uint32_t acuiredIndex() const { return mAcquiredIndex; }; + 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; }; + + void acquire(osg::GraphicsContext* gc); + void blitAndRelease(osg::GraphicsContext* gc, VRFramebuffer& readBuffer); + void checkAcquired() const; + + protected: + + private: + SwapchainConfig mConfig; + XrSwapchain mSwapchain = XR_NULL_HANDLE; + XrSwapchainSubImage mSubImage{}; + std::vector< std::unique_ptr > mImages; + int32_t mWidth = -1; + int32_t mHeight = -1; + int32_t mSamples = -1; + int64_t mFormat = 0; + uint32_t mAcquiredIndex{ 0 }; + Use mUsage; + bool mIsIndexAcquired{ false }; + bool mIsReady{ false }; + }; + + public: + OpenXRSwapchainImpl(osg::ref_ptr state, SwapchainConfig config); + ~OpenXRSwapchainImpl(); + + void beginFrame(osg::GraphicsContext* gc); + void endFrame(osg::GraphicsContext* gc, VRFramebuffer& readBuffer); + + bool isAcquired() const; + XrSwapchain xrSwapchain(void) const; + XrSwapchain xrSwapchainDepth(void) const; + XrSwapchainSubImage xrSubImage(void) const; + XrSwapchainSubImage xrSubImageDepth(void) const; + int width() const { return mConfig.selectedWidth; }; + int height() const { return mConfig.selectedHeight; }; + int samples() const { return mConfig.selectedSamples; }; + + protected: + OpenXRSwapchainImpl(const OpenXRSwapchainImpl&) = delete; + void operator=(const OpenXRSwapchainImpl&) = delete; + + void acquire(osg::GraphicsContext* gc); + void release(osg::GraphicsContext* gc, VRFramebuffer& readBuffer); + void checkAcquired() const; + + private: + SwapchainConfig mConfig; + std::unique_ptr mSwapchain{ nullptr }; + std::unique_ptr mSwapchainDepth{ nullptr }; + bool mFormallyAcquired{ false }; + bool mShouldRelease{ false }; + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrtracker.cpp b/apps/openmw/mwvr/openxrtracker.cpp new file mode 100644 index 000000000..3741072dc --- /dev/null +++ b/apps/openmw/mwvr/openxrtracker.cpp @@ -0,0 +1,141 @@ +#include "openxrinput.hpp" +#include "openxrmanagerimpl.hpp" +#include "openxrplatform.hpp" +#include "openxrtracker.hpp" +#include "openxrtypeconversions.hpp" +#include "vrenvironment.hpp" +#include "vrinputmanager.hpp" +#include "vrsession.hpp" + +#include + +namespace MWVR +{ + OpenXRTracker::OpenXRTracker(const std::string& name, XrSpace referenceSpace) + : VRTrackingSource(name) + , mReferenceSpace(referenceSpace) + , mTrackingSpaces() + { + + } + + OpenXRTracker::~OpenXRTracker() + { + } + + void OpenXRTracker::addTrackingSpace(VRPath path, XrSpace space) + { + mTrackingSpaces[path] = space; + notifyAvailablePosesChanged(); + } + + void OpenXRTracker::deleteTrackingSpace(VRPath path) + { + mTrackingSpaces.erase(path); + notifyAvailablePosesChanged(); + } + + void OpenXRTracker::setReferenceSpace(XrSpace referenceSpace) + { + mReferenceSpace = referenceSpace; + } + + std::vector OpenXRTracker::listSupportedTrackingPosePaths() const + { + std::vector path; + for (auto& e : mTrackingSpaces) + path.push_back(e.first); + return path; + } + + void OpenXRTracker::updateTracking(DisplayTime predictedDisplayTime) + { + Environment::get().getInputManager()->xrInput().getActionSet(ActionSet::Tracking).updateControls(); + auto* xr = Environment::get().getManager(); + auto* session = Environment::get().getSession(); + + auto& frame = session->getFrame(VRSession::FramePhase::Update); + frame->mViews[(int)ReferenceSpace::STAGE] = locateViews(predictedDisplayTime, xr->impl().getReferenceSpace(ReferenceSpace::STAGE)); + frame->mViews[(int)ReferenceSpace::VIEW] = locateViews(predictedDisplayTime, xr->impl().getReferenceSpace(ReferenceSpace::VIEW)); + } + + XrSpace OpenXRTracker::getSpace(VRPath path) + { + auto it = mTrackingSpaces.find(path); + if (it != mTrackingSpaces.end()) + return it->second; + return 0; + } + + VRTrackingPose OpenXRTracker::getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath reference) + { + VRTrackingPose pose; + pose.status = TrackingStatus::Good; + XrSpace space = getSpace(path); + XrSpace ref = reference == 0 ? mReferenceSpace : getSpace(reference); + if (space == 0 || ref == 0) + pose.status = TrackingStatus::NotTracked; + if (!!pose.status) + locate(pose, space, ref, predictedDisplayTime); + return pose; + } + + void OpenXRTracker::locate(VRTrackingPose& pose, XrSpace space, XrSpace reference, DisplayTime predictedDisplayTime) + { + XrSpaceLocation location{ XR_TYPE_SPACE_LOCATION }; + auto res = xrLocateSpace(space, mReferenceSpace, predictedDisplayTime, &location); + + if (XR_FAILED(res)) + { + // Call failed, exit. + CHECK_XRRESULT(res, "xrLocateSpace"); + pose.status = TrackingStatus::RuntimeFailure; + return; + } + + // Check that everything is being tracked + if (!(location.locationFlags & (XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT | XR_SPACE_LOCATION_POSITION_TRACKED_BIT))) + { + // It's not, data is stale + pose.status = TrackingStatus::Stale; + } + + // Check that data is valid + if (!(location.locationFlags & (XR_SPACE_LOCATION_ORIENTATION_VALID_BIT | XR_SPACE_LOCATION_POSITION_VALID_BIT))) + { + // It's not, we've lost tracking + pose.status = TrackingStatus::Lost; + } + + pose.pose = MWVR::Pose{ + fromXR(location.pose.position), + fromXR(location.pose.orientation) + }; + } + + std::array OpenXRTracker::locateViews(DisplayTime predictedDisplayTime, XrSpace reference) + { + std::array xrViews{ {{XR_TYPE_VIEW}, {XR_TYPE_VIEW}} }; + XrViewState viewState{ XR_TYPE_VIEW_STATE }; + uint32_t viewCount = 2; + + XrViewLocateInfo viewLocateInfo{ XR_TYPE_VIEW_LOCATE_INFO }; + viewLocateInfo.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + viewLocateInfo.displayTime = predictedDisplayTime; + viewLocateInfo.space = reference; + + auto* xr = Environment::get().getManager(); + CHECK_XRCMD(xrLocateViews(xr->impl().xrSession(), &viewLocateInfo, &viewState, viewCount, &viewCount, xrViews.data())); + + std::array vrViews{}; + vrViews[(int)Side::LEFT_SIDE].pose = fromXR(xrViews[(int)Side::LEFT_SIDE].pose); + vrViews[(int)Side::RIGHT_SIDE].pose = fromXR(xrViews[(int)Side::RIGHT_SIDE].pose); + vrViews[(int)Side::LEFT_SIDE].fov = fromXR(xrViews[(int)Side::LEFT_SIDE].fov); + vrViews[(int)Side::RIGHT_SIDE].fov = fromXR(xrViews[(int)Side::RIGHT_SIDE].fov); + return vrViews; + } + + OpenXRTrackingToWorldBinding::OpenXRTrackingToWorldBinding() + { + } +} diff --git a/apps/openmw/mwvr/openxrtracker.hpp b/apps/openmw/mwvr/openxrtracker.hpp new file mode 100644 index 000000000..c3cee6f98 --- /dev/null +++ b/apps/openmw/mwvr/openxrtracker.hpp @@ -0,0 +1,86 @@ +#ifndef OPENXR_TRACKER_HPP +#define OPENXR_TRACKER_HPP + +#include +#include "vrtracking.hpp" + +#include + +namespace MWVR +{ + //! Serves as a C++ wrapper of openxr spaces, but also bridges stage coordinates and game coordinates. + //! Supports the compulsory sets of paths. + class OpenXRTracker : public VRTrackingSource + { + public: + OpenXRTracker(const std::string& name, XrSpace referenceSpace); + ~OpenXRTracker(); + + void addTrackingSpace(VRPath path, XrSpace space); + void deleteTrackingSpace(VRPath path); + + //! The base space used to reference everything else. + void setReferenceSpace(XrSpace referenceSpace); + + std::vector listSupportedTrackingPosePaths() const override; + void updateTracking(DisplayTime predictedDisplayTime) override; + + protected: + VRTrackingPose getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath reference = 0) override; + + private: + std::array locateViews(DisplayTime predictedDisplayTime, XrSpace reference); + void locate(VRTrackingPose& pose, XrSpace space, XrSpace reference, DisplayTime predictedDisplayTime); + XrSpace getSpace(VRPath); + + XrSpace mReferenceSpace; + std::map mTrackingSpaces; + }; + + //! Ties a tracked pose to the game world. + //! A movement tracking pose is selected by passing its path to the constructor. + //! All poses are transformed in the horizontal plane by moving the x,y origin to the position of the movement tracking pose, and then reoriented using the current orientation. + //! The movement tracking pose is effectively always at the x,y origin + //! The movement of the movement tracking pose is accumulated and can be read using the movement() call. + //! If this movement is ever consumed (such as by moving the character to follow the player) the consumed movement must be reported using consumeMovement(). + class OpenXRTrackingToWorldBinding + { + public: + OpenXRTrackingToWorldBinding(); + + //! Re-orient the stage. + void setOrientation(float yaw, bool adjust); + osg::Quat getOrientation() const { return mOrientation; } + + void setEyeLevel(float eyeLevel) { mEyeLevel = eyeLevel; } + float getEyeLevel() const { return mEyeLevel; } + + void setSeatedPlay(bool seatedPlay) { mSeatedPlay = seatedPlay; } + bool getSeatedPlay() const { return mSeatedPlay; } + + //! The player's movement within the VR stage. This accumulates until the movement has been consumed by calling consumeMovement() + osg::Vec3 movement() const; + + //! Consume movement + void consumeMovement(const osg::Vec3& movement); + + //! Recenter tracking by consuming all movement. + void recenter(bool resetZ); + + void update(Pose movementTrackingPose); + + //! Transforms a stage-referenced pose to be world-aligned. + //! \note Unlike VRTrackingSource::getTrackingPose() this does not take a reference path, as re-alignment is only needed when fetching a stage-referenced pose. + void alignPose(Pose& pose); + + private: + bool mSeatedPlay = false; + bool mHasTrackingData = false; + float mEyeLevel = 0; + Pose mLastPose = Pose(); + osg::Vec3 mMovement = osg::Vec3(0,0,0); + osg::Quat mOrientation = osg::Quat(0,0,0,1); + }; +} + +#endif diff --git a/apps/openmw/mwvr/openxrtypeconversions.cpp b/apps/openmw/mwvr/openxrtypeconversions.cpp new file mode 100644 index 000000000..22bfd9980 --- /dev/null +++ b/apps/openmw/mwvr/openxrtypeconversions.cpp @@ -0,0 +1,76 @@ +#include "openxrtypeconversions.hpp" +#include "openxrswapchain.hpp" +#include "openxrswapchainimpl.hpp" +#include + +namespace MWVR +{ + osg::Vec3 fromXR(XrVector3f v) + { + return osg::Vec3{ v.x, -v.z, v.y }; + } + + osg::Quat fromXR(XrQuaternionf quat) + { + return osg::Quat{ quat.x, -quat.z, quat.y, quat.w }; + } + + XrVector3f toXR(osg::Vec3 v) + { + return XrVector3f{ v.x(), v.z(), -v.y() }; + } + + XrQuaternionf toXR(osg::Quat quat) + { + return XrQuaternionf{ static_cast(quat.x()), static_cast(quat.z()), static_cast(-quat.y()), static_cast(quat.w()) }; + } + + MWVR::Pose fromXR(XrPosef pose) + { + return MWVR::Pose{ fromXR(pose.position), fromXR(pose.orientation) }; + } + + XrPosef toXR(MWVR::Pose pose) + { + return XrPosef{ toXR(pose.orientation), toXR(pose.position) }; + } + + MWVR::FieldOfView fromXR(XrFovf fov) + { + return MWVR::FieldOfView{ fov.angleLeft, fov.angleRight, fov.angleUp, fov.angleDown }; + } + + XrFovf toXR(MWVR::FieldOfView fov) + { + return XrFovf{ fov.angleLeft, fov.angleRight, fov.angleUp, fov.angleDown }; + } + + XrCompositionLayerProjectionView toXR(MWVR::CompositionLayerProjectionView layer) + { + XrCompositionLayerProjectionView xrLayer; + xrLayer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW; + xrLayer.subImage = toXR(layer.subImage, false); + xrLayer.pose = toXR(layer.pose); + xrLayer.fov = toXR(layer.fov); + xrLayer.next = nullptr; + + return xrLayer; + } + + XrSwapchainSubImage toXR(MWVR::SubImage subImage, bool depthImage) + { + XrSwapchainSubImage xrSubImage{}; + if (depthImage) + xrSubImage.swapchain = subImage.swapchain->impl().xrSwapchainDepth(); + else + xrSubImage.swapchain = subImage.swapchain->impl().xrSwapchain(); + xrSubImage.imageRect.extent.width = subImage.width; + xrSubImage.imageRect.extent.height = subImage.height; + xrSubImage.imageRect.offset.x = subImage.x; + xrSubImage.imageRect.offset.y = subImage.y; + xrSubImage.imageArrayIndex = 0; + return xrSubImage; + } +} + + diff --git a/apps/openmw/mwvr/openxrtypeconversions.hpp b/apps/openmw/mwvr/openxrtypeconversions.hpp new file mode 100644 index 000000000..0e3579ef2 --- /dev/null +++ b/apps/openmw/mwvr/openxrtypeconversions.hpp @@ -0,0 +1,25 @@ +#ifndef MWVR_OPENXRTYPECONVERSIONS_H +#define MWVR_OPENXRTYPECONVERSIONS_H + +#include +#include "vrtypes.hpp" +#include +#include + +namespace MWVR +{ + /// Conversion methods between openxr types to osg/mwvr types. Includes managing the differing conventions. + Pose fromXR(XrPosef pose); + FieldOfView fromXR(XrFovf fov); + osg::Vec3 fromXR(XrVector3f); + osg::Quat fromXR(XrQuaternionf quat); + XrPosef toXR(Pose pose); + XrFovf toXR(FieldOfView fov); + XrVector3f toXR(osg::Vec3 v); + XrQuaternionf toXR(osg::Quat quat); + + XrCompositionLayerProjectionView toXR(CompositionLayerProjectionView layer); + XrSwapchainSubImage toXR(SubImage, bool depthImage); +} + +#endif diff --git a/apps/openmw/mwvr/realisticcombat.cpp b/apps/openmw/mwvr/realisticcombat.cpp new file mode 100644 index 000000000..31b7e1df7 --- /dev/null +++ b/apps/openmw/mwvr/realisticcombat.cpp @@ -0,0 +1,347 @@ +#include "realisticcombat.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" + +#include "../mwmechanics/weapontype.hpp" + +#include + +#include + +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, VRPath trackingPath) + : mPtr(ptr) + , mMinVelocity(Settings::Manager::getFloat("realistic combat minimum swing velocity", "VR")) + , mMaxVelocity(Settings::Manager::getFloat("realistic combat maximum swing velocity", "VR")) + , mTrackingPath(trackingPath) + { + Log(Debug::Verbose) << "realistic combat minimum swing velocity: " << mMinVelocity; + Log(Debug::Verbose) << "realistic combat maximum swing velocity: " << mMaxVelocity; + Environment::get().getTrackingManager()->bind(this, "pcstage"); + } + + void StateMachine::onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) + { + mTrackingInput = source.getTrackingPose(predictedDisplayTime, mTrackingPath); + } + + bool StateMachine::canSwing() + { + if (mSwingType >= 0) + if (mVelocity >= mMinVelocity) + if (mSwingType != ESM::Weapon::AT_Thrust || mThrustVelocity >= 0.f) + return true; + return false; + } + + // Actions common to all transitions + void StateMachine::transition( + SwingState newState) + { + Log(Debug::Verbose) << "Transition:" << stateToString(mState) << "->" << stateToString(newState); + mMaxSwingVelocity = 0.f; + mTimeSinceEnteredState = 0.f; + mMovementSinceEnteredState = 0.f; + mState = newState; + } + + void StateMachine::reset() + { + mMaxSwingVelocity = 0.f; + mTimeSinceEnteredState = 0.f; + mVelocity = 0.f; + mPreviousPosition = osg::Vec3(0.f, 0.f, 0.f); + mState = 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* world = MWBase::Environment::get().getWorld(); + auto& handPose = mTrackingInput.pose; + auto weaponType = world->getActiveWeaponType(); + + enabled = enabled && isMeleeWeapon(weaponType); + enabled = enabled && !!mTrackingInput.status; + + if (mEnabled != enabled) + { + reset(); + mEnabled = enabled; + } + if (!enabled) + return; + + mTimeSinceEnteredState += 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 (mPreviousPosition == osg::Vec3(0.f, 0.f, 0.f)) + mPreviousPosition = handPose.position; + + osg::Vec3 movement = handPose.position - mPreviousPosition; + mMovementSinceEnteredState += movement.length(); + mPreviousPosition = 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. + mThrustVelocity = swingVector * thrustDirection; + mVelocity = swingVector.length(); + + + if (isSideSwingValidForWeapon(weaponType)) + { + // Compute velocity in the plane normal to the thrust direction. + float thrustComponent = std::abs(mThrustVelocity / mVelocity); + float planeComponent = std::sqrt(1 - thrustComponent * thrustComponent); + mSlashChopVelocity = mVelocity * planeComponent; + mSideVelocity = -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. + mSlashChopVelocity = std::abs(swingVector * slashChopDirection); + mSideVelocity = 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 (mSideVelocity > std::abs(mThrustVelocity) && mSideVelocity > mSlashChopVelocity) + { + // Player is swinging with the "blunt" side of a weapon that + // cannot be used that way. + mSwingType = -1; + } + else if (std::abs(mThrustVelocity) > mSlashChopVelocity) + { + mSwingType = 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) + mSwingType = 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) + mSwingType = ESM::Weapon::AT_Chop; + else + mSwingType = ESM::Weapon::AT_Slash; + } + } + + switch (mState) + { + 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(mState) + " ya dingus"); + } + } + + void StateMachine::update_cooldownState() + { + if (mTimeSinceEnteredState >= mMinimumPeriod) + 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 (mStrength < 0.5f) + sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack + if (mStrength < 1.0f) + sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack + else + sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack + + Log(Debug::Verbose) << "Swing: " << swingTypeToString(mSwingType); + } + + void StateMachine::update_launchState() + { + if (mMovementSinceEnteredState > mMinimumPeriod) + 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() + { + mMaxSwingVelocity = std::max(mVelocity, mMaxSwingVelocity); + mStrength = std::min(1.f, (mMaxSwingVelocity - mMinVelocity) / mMaxVelocity); + + // 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 (mPtr.getClass().hit(mPtr, mStrength, mSwingType, true)) + return transition_swingingToImpact(); + } + + void StateMachine::transition_swingingToImpact() + { + mPtr.getClass().hit(mPtr, mStrength, mSwingType, false); + transition(SwingState_Impact); + } + + void StateMachine::update_impactState() + { + if (mVelocity < mMinVelocity) + return transition_impactToCooldown(); + } + + void StateMachine::transition_impactToCooldown() + { + transition(SwingState_Cooldown); + } + + } +} diff --git a/apps/openmw/mwvr/realisticcombat.hpp b/apps/openmw/mwvr/realisticcombat.hpp new file mode 100644 index 000000000..f5e7faba5 --- /dev/null +++ b/apps/openmw/mwvr/realisticcombat.hpp @@ -0,0 +1,115 @@ +#ifndef MWVR_REALISTICCOMBAT_H +#define MWVR_REALISTICCOMBAT_H + +#include + +#include "../mwbase/world.hpp" +#include "../mwworld/ptr.hpp" +#include "../mwworld/class.hpp" + +#include "vrenvironment.hpp" +#include "vrsession.hpp" +#include "vrtracking.hpp" + +namespace MWVR { + namespace RealisticCombat { + + /// Enum describing the current state of the MWVR::RealisticCombat::StateMachine + enum SwingState + { + SwingState_Ready, + SwingState_Launch, + SwingState_Swing, + SwingState_Impact, + SwingState_Cooldown, + }; + + ///////////////////////////////////////////////////////////////////// + /// \brief State machine for "realistic" combat in openmw vr + /// + /// \sa SwingState + /// + /// Initial state: Ready. + /// + /// State Ready: Ready to initiate a new attack. + /// State Launch: Player has begun swinging his weapon. + /// State Swing: Currently swinging weapon. + /// State Impact: Contact made, weapon still swinging. + /// State Cooldown: Swing completed, wait a minimum period before next. + /// + /// Transition rules: + /// Ready -> Launch: When the minimum velocity of swing is achieved. + /// Launch -> Ready: When the minimum velocity of swing is lost before minimum distance was swung. + /// Launch -> Swing: When minimum distance is swung. + /// - Play Swish sound + /// Swing -> Impact: When minimum velocity is lost, or when a hit is detected. + /// - Register hit based on max velocity observed in swing state + /// Impact -> Cooldown: When velocity returns below minimum. + /// Cooldown -> Ready: When the minimum period has passed since entering Cooldown state + /// + /// + struct StateMachine : public VRTrackingListener + { + public: + StateMachine(MWWorld::Ptr ptr, VRPath trackingPath); + void update(float dt, bool enabled); + MWWorld::Ptr ptr() { return mPtr; } + + protected: + void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) override; + + bool canSwing(); + + void playSwish(); + void reset(); + + void transition(SwingState newState); + + void update_cooldownState(); + void transition_cooldownToReady(); + + void update_readyState(); + void transition_readyToLaunch(); + + void update_launchState(); + void transition_launchToReady(); + void transition_launchToSwing(); + + void update_swingState(); + void transition_swingingToImpact(); + + void update_impactState(); + void transition_impactToCooldown(); + + private: + MWWorld::Ptr mPtr; + const float mMinVelocity; + const float mMaxVelocity; + + float mVelocity = 0.f; + float mMaxSwingVelocity = 0.f; + + SwingState mState = SwingState_Ready; + int mSwingType = -1; + float mStrength = 0.f; + + float mThrustVelocity{ 0.f }; + float mSlashChopVelocity{ 0.f }; + float mSideVelocity{ 0.f }; + + float mMinimumPeriod{ .25f }; + + float mTimeSinceEnteredState = { 0.f }; + float mMovementSinceEnteredState = { 0.f }; + + bool mEnabled = false; + + osg::Vec3 mPreviousPosition{ 0.f,0.f,0.f }; + VRTrackingPose mTrackingInput = VRTrackingPose(); + VRPath mTrackingPath = 0; + }; + + } +} + +#endif diff --git a/apps/openmw/mwvr/vranimation.cpp b/apps/openmw/mwvr/vranimation.cpp new file mode 100644 index 000000000..01b58c12d --- /dev/null +++ b/apps/openmw/mwvr/vranimation.cpp @@ -0,0 +1,552 @@ +#include "vranimation.hpp" +#include "vrenvironment.hpp" +#include "vrviewer.hpp" +#include "vrinputmanager.hpp" +#include "vrcamera.hpp" +#include "vrutil.hpp" +#include "vrpointer.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "../mwworld/esmstore.hpp" + +#include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/actorutil.hpp" +#include "../mwmechanics/weapontype.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwrender/camera.hpp" +#include "../mwrender/renderingmanager.hpp" +#include "../mwrender/vismask.hpp" + +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; + + return false; + } + + /// 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) + { + traverse(node, nv); + return; + } + + // 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); + + + // 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 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; + }; + + 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); + 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) + { + 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; + } + + auto matrixTransform = node->asTransform()->asMatrixTransform(); + auto matrix = matrixTransform->getMatrix(); + matrix.setRotate(rotate); + matrixTransform->setMatrix(matrix); + + 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) + { + 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); + } + + class TrackingController : public VRTrackingListener + { + public: + TrackingController(VRPath trackingPath, osg::Vec3 baseOffset, osg::Quat baseOrientation) + : mTrackingPath(trackingPath) + , mTransform(nullptr) + , mBaseOffset(baseOffset) + , mBaseOrientation(baseOrientation) + { + + } + + void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) override + { + if (!mTransform) + return; + + auto tp = source.getTrackingPose(predictedDisplayTime, mTrackingPath, 0); + if (!tp.status) + return; + + auto orientation = mBaseOrientation * tp.pose.orientation; + + // Undo the wrist translate + // TODO: I'm sure this could bee a lot less hacky + // But i'll defer that to whenever we get inverse cinematics so i can track the hand directly. + auto* hand = mTransform->getChild(0); + auto handMatrix = hand->asTransform()->asMatrixTransform()->getMatrix(); + auto position = tp.pose.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 * mBaseOffset; + + // Get current world transform of limb + osg::Matrix worldToLimb = osg::computeWorldToLocal(mTransform->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 + mTransform->setMatrix(worldReference * worldToLimb * mTransform->getMatrix()); + } + + void setTransform(osg::MatrixTransform* transform) + { + mTransform = transform; + } + + VRPath mTrackingPath; + osg::ref_ptr mTransform; + osg::Vec3 mBaseOffset; + osg::Quat mBaseOrientation; + }; + + VRAnimation::VRAnimation( + const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem, + bool disableSounds, std::shared_ptr userPointer) + // 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) + , 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)))) + , mUserPointer(userPointer) + { + for (int i = 0; i < 2; i++) + { + mIndexFingerControllers[i] = new FingerController; + mHandControllers[i] = new HandController; + } + + mWeaponDirectionTransform = new osg::MatrixTransform(); + mWeaponDirectionTransform->setName("Weapon Direction"); + mWeaponDirectionTransform->setUpdateCallback(new WeaponDirectionController); + + mModelOffset->setName("ModelOffset"); + + mWeaponPointerTransform = new osg::MatrixTransform(); + mWeaponPointerTransform->setMatrix( + osg::Matrix::scale(0.f, 0.f, 0.f) + ); + mWeaponPointerTransform->setName("Weapon Pointer"); + mWeaponPointerTransform->setUpdateCallback(new WeaponPointerController); + //mWeaponDirectionTransform->addChild(mWeaponPointerTransform); + + auto vrTrackingManager = MWVR::Environment::get().getTrackingManager(); + vrTrackingManager->bind(this, "pcworld"); + auto* source = static_cast(vrTrackingManager->getSource("pcworld")); + source->setOriginNode(mObjectRoot->getParent(0)); + + // Morrowind's meshes do not point forward by default and need re-positioning and orientation. + float VRbias = osg::DegreesToRadians(-90.f); + osg::Quat yaw(-VRbias, osg::Vec3f(0, 0, 1)); + osg::Quat roll(2 * VRbias, osg::Vec3f(1, 0, 0)); + osg::Vec3 offset{ 15,0,0 }; + auto* tm = Environment::get().getTrackingManager(); + + // Note that these controllers could be bound directly to source in the tracking manager. + // Instead we store them and update them manually to ensure order of operations. + { + auto path = tm->stringToVRPath("/user/hand/right/input/aim/pose"); + auto orientation = yaw; + mVrControllers.emplace("bip01 r forearm", std::make_unique(path, offset, orientation)); + } + + { + auto path = tm->stringToVRPath("/user/hand/left/input/aim/pose"); + auto orientation = roll * yaw; + mVrControllers.emplace("bip01 l forearm", std::make_unique(path, offset, orientation)); + } + } + + 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 hands + 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); + removeIndividualPart(ESM::PartReferenceType::PRT_LPauldron); + removeIndividualPart(ESM::PartReferenceType::PRT_RPauldron); + } + 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.8288f; // Is this ~ the right value? + float charHeight = charHeightBase * charHeightFactor; + float realHeight = Settings::Manager::getFloat("real height", "VR"); + float sizeFactor = charHeight / realHeight; + Environment::get().getSession()->setPlayerScale(sizeFactor); + Environment::get().getSession()->setEyeLevel(charHeightBase - 0.15f); // approximation + } + + 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]); + if (enabled) + { + base_joint->addUpdateCallback(mIndexFingerControllers[0]); + second_joint->addUpdateCallback(mIndexFingerControllers[1]); + + } + } + + mUserPointer->setEnabled(enabled); + + mFingerPointingMode = enabled; + } + + float VRAnimation::getVelocity(const std::string& groupname) const + { + return 0.0f; + } + + void VRAnimation::onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) + { + for (auto& controller : mVrControllers) + controller.second->onTrackingUpdated(source, predictedDisplayTime); + + if (mSkeleton) + mSkeleton->markBoneMatriceDirty(); + + mUserPointer->updatePointerTarget(); + } + + 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 controller = mVrControllers.find(forearm->first); + if (controller != mVrControllers.end()) + { + controller->second->setTransform(forearm->second); + } + } + + 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()) + { + hand->second->removeChild(mWeaponDirectionTransform); + hand->second->addChild(mWeaponDirectionTransform); + } + auto finger = mNodeMap.find("bip01 r finger11"); + if (finger != mNodeMap.end()) + { + mUserPointer->setParent(finger->second); + } + mSkeleton->setIsTracked(true); + } + void VRAnimation::enableHeadAnimation(bool) + { + NpcAnimation::enableHeadAnimation(false); + } + void VRAnimation::setAccurateAiming(bool) + { + NpcAnimation::setAccurateAiming(false); + } + + osg::Matrix VRAnimation::getWeaponTransformMatrix() const + { + return osg::computeLocalToWorld(mWeaponDirectionTransform->getParentalNodePaths()[0]); + } +} diff --git a/apps/openmw/mwvr/vranimation.hpp b/apps/openmw/mwvr/vranimation.hpp new file mode 100644 index 000000000..641ba2cfe --- /dev/null +++ b/apps/openmw/mwvr/vranimation.hpp @@ -0,0 +1,82 @@ +#ifndef MWVR_VRANIMATION_H +#define MWVR_VRANIMATION_H + +#include "../mwrender/npcanimation.hpp" +#include "../mwrender/renderingmanager.hpp" +#include "openxrmanager.hpp" +#include "vrsession.hpp" +#include "vrtracking.hpp" + +namespace MWVR +{ + class HandController; + class FingerController; + class TrackingController; + class UserPointer; + + /// Subclassing NpcAnimation to implement VR related behaviour + class VRAnimation : public MWRender::NpcAnimation, public VRTrackingListener + { + 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 userPointer); + virtual ~VRAnimation(); + + /// Overridden to always be false + void enableHeadAnimation(bool enable) override; + + /// Overridden to always be false + void setAccurateAiming(bool enabled) override; + + /// Overridden, implementation tbd + osg::Vec3f runAnimation(float timepassed) override; + + /// Overriden to always be a variant of VM_VR* + void setViewMode(ViewMode viewMode) override; + + /// Overriden to include VR modifications + void updateParts() override; + + /// Overrides finger animations to point forward + void setFingerPointingMode(bool enabled); + + /// @return Whether animation is currently in finger pointing mode + bool fingerPointingMode() const { return mFingerPointingMode; } + + /// @return world transform that yields the position and orientation of the current weapon + osg::Matrix getWeaponTransformMatrix() const; + + protected: + + float getVelocity(const std::string& groupname) const override; + + void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) override; + + protected: + std::shared_ptr mSession; + std::map > mVrControllers; + osg::ref_ptr mHandControllers[2]; + osg::ref_ptr mIndexFingerControllers[2]; + osg::ref_ptr mModelOffset; + + bool mFingerPointingMode{ false }; + std::shared_ptr mUserPointer; + osg::ref_ptr mWeaponDirectionTransform{ nullptr }; + osg::ref_ptr mWeaponPointerTransform{ nullptr }; + }; + +} + +#endif diff --git a/apps/openmw/mwvr/vrcamera.cpp b/apps/openmw/mwvr/vrcamera.cpp new file mode 100644 index 000000000..493e5d685 --- /dev/null +++ b/apps/openmw/mwvr/vrcamera.cpp @@ -0,0 +1,211 @@ +#include "vrcamera.hpp" +#include "vrgui.hpp" +#include "vrinputmanager.hpp" +#include "vrenvironment.hpp" +#include "vranimation.hpp" + +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwworld/player.hpp" +#include "../mwworld/class.hpp" + +#include "../mwmechanics/movement.hpp" + +#include + +namespace MWVR +{ + + VRCamera::VRCamera(osg::Camera* camera) + : MWRender::Camera(camera) + { + mVanityAllowed = false; + mFirstPersonView = true; + + auto vrTrackingManager = MWVR::Environment::get().getTrackingManager(); + vrTrackingManager->bind(this, "pcworld"); + } + + VRCamera::~VRCamera() + { + } + + void VRCamera::setShouldTrackPlayerCharacter(bool track) + { + mShouldTrackPlayerCharacter = track; + } + + void VRCamera::recenter() + { + if (!mHasTrackingData) + return; + + // Move position of head to center of character + // Z should not be affected + + auto* session = Environment::get().getSession(); + + auto* tm = Environment::get().getTrackingManager(); + auto* ws = static_cast(tm->getSource("pcworld")); + + + ws->setSeatedPlay(session->seatedPlay()); + ws->setEyeLevel(session->eyeLevel() * Constants::UnitsPerMeter); + ws->recenter(mShouldResetZ); + + + + mShouldRecenter = false; + Log(Debug::Verbose) << "Recentered"; + } + + void VRCamera::applyTracking() + { + 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); + + if (!player.isDisabled() && mTrackingNode) + { + world->rotateObject(playerPtr, pitch, 0.f, yaw, MWBase::RotationFlag_none); + } + } + + void VRCamera::onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) + { + auto path = Environment::get().getTrackingManager()->stringToVRPath("/user/head/input/pose"); + auto tp = source.getTrackingPose(predictedDisplayTime, path); + + if (!!tp.status) + { + mHeadPose = tp.pose; + mHasTrackingData = true; + } + + if (mShouldRecenter) + { + recenter(); + Camera::updateCamera(mCamera); + auto* vrGuiManager = MWVR::Environment::get().getGUIManager(); + vrGuiManager->updateTracking(); + } + else + { + if (mShouldTrackPlayerCharacter && !MWBase::Environment::get().getWindowManager()->isGuiMode()) + applyTracking(); + + Camera::updateCamera(mCamera); + } + } + + void VRCamera::updateCamera(osg::Camera* cam) + { + // The regular update call should do nothing while tracking the player + } + + void VRCamera::updateCamera() + { + Camera::updateCamera(); + } + + void VRCamera::reset() + { + Camera::reset(); + } + + void VRCamera::rotateCamera(float pitch, float roll, float yaw, bool adjust) + { + if (adjust) + { + pitch += getPitch(); + yaw += getYaw(); + } + setYaw(yaw); + setPitch(pitch); + } + + void VRCamera::toggleViewMode(bool force) + { + mFirstPersonView = true; + } + bool VRCamera::toggleVanityMode(bool enable) + { + // Vanity mode makes no sense in VR + return Camera::toggleVanityMode(false); + } + void VRCamera::allowVanityMode(bool allow) + { + // Vanity mode makes no sense in VR + mVanityAllowed = false; + } + void VRCamera::getPosition(osg::Vec3d& focal, osg::Vec3d& camera) const + { + camera = focal = mHeadPose.position; + } + void VRCamera::getOrientation(osg::Quat& orientation) const + { + orientation = mHeadPose.orientation; + } + + void VRCamera::processViewChange() + { + SceneUtil::FindByNameVisitor findRootVisitor("Player Root", osg::NodeVisitor::TRAVERSE_PARENTS); + mAnimation->getObjectRoot()->accept(findRootVisitor); + mTrackingNode = findRootVisitor.mFoundNode; + + if (!mTrackingNode) + throw std::logic_error("Unable to find tracking node for VR camera"); + mHeightScale = 1.f; + } + + void VRCamera::instantTransition() + { + Camera::instantTransition(); + + // When the cell changes, openmw rotates the character. + // To make sure the player faces the same direction regardless of current orientation, + // compute the offset from character orientation to player orientation and reset yaw offset to this. + float yaw = 0.f; + float pitch = 0.f; + float roll = 0.f; + getEulerAngles(mHeadPose.orientation, yaw, pitch, roll); + yaw = - mYaw - yaw; + auto* tm = Environment::get().getTrackingManager(); + auto* ws = static_cast(tm->getSource("pcworld")); + ws->setWorldOrientation(yaw, true); + } + + void VRCamera::rotateStage(float yaw) + { + auto* tm = Environment::get().getTrackingManager(); + auto* ws = static_cast(tm->getSource("pcworld")); + ws->setWorldOrientation(yaw, true); + } + + osg::Quat VRCamera::stageRotation() + { + auto* tm = Environment::get().getTrackingManager(); + auto* ws = static_cast(tm->getSource("pcworld")); + return ws->getWorldOrientation(); + } + + void VRCamera::requestRecenter(bool resetZ) + { + mShouldRecenter = true; + + // Use OR so we don't negate a pending requests. + mShouldResetZ |= resetZ; + } +} diff --git a/apps/openmw/mwvr/vrcamera.hpp b/apps/openmw/mwvr/vrcamera.hpp new file mode 100644 index 000000000..53f3adacf --- /dev/null +++ b/apps/openmw/mwvr/vrcamera.hpp @@ -0,0 +1,77 @@ +#ifndef GAME_MWVR_VRCAMERA_H +#define GAME_MWVR_VRCAMERA_H + +#include + +#include +#include +#include +#include + +#include "../mwrender/camera.hpp" +#include "openxrtracker.hpp" + +#include "vrtypes.hpp" + +namespace MWVR +{ + /// \brief VR camera control + class VRCamera : public MWRender::Camera, public VRTrackingListener + { + public: + + VRCamera(osg::Camera* camera); + ~VRCamera() override; + + /// Update the view matrix of \a cam + void updateCamera(osg::Camera* cam) override; + + /// Update the view matrix of the current camera + void updateCamera() override; + + /// Reset to defaults + void reset() override; + + /// Set where the camera is looking at. Uses Morrowind (euler) angles + /// \param rot Rotation angles in radians + void rotateCamera(float pitch, float roll, float yaw, bool adjust) override; + + void toggleViewMode(bool force = false) override; + + bool toggleVanityMode(bool enable) override; + void allowVanityMode(bool allow) override; + + /// Stores focal and camera world positions in passed arguments + void getPosition(osg::Vec3d& focal, osg::Vec3d& camera) const override; + + /// Store camera orientation in passed arguments + void getOrientation(osg::Quat& orientation) const override; + + void processViewChange() override; + + void instantTransition() override; + + osg::Quat stageRotation(); + + void rotateStage(float yaw); + + void requestRecenter(bool resetZ); + + void setShouldTrackPlayerCharacter(bool track); + + protected: + void recenter(); + void applyTracking(); + + void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) override; + + private: + Pose mHeadPose{}; + bool mShouldRecenter{ true }; + bool mShouldResetZ{ true }; + bool mHasTrackingData{ false }; + bool mShouldTrackPlayerCharacter{ false }; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrenvironment.cpp b/apps/openmw/mwvr/vrenvironment.cpp new file mode 100644 index 000000000..6915559a5 --- /dev/null +++ b/apps/openmw/mwvr/vrenvironment.cpp @@ -0,0 +1,117 @@ +#include "vrenvironment.hpp" + +#include + +#include "vranimation.hpp" +#include "vrinputmanager.hpp" +#include "vrsession.hpp" +#include "vrgui.hpp" + +#include "../mwbase/environment.hpp" + +MWVR::Environment* MWVR::Environment::sThis = 0; + +MWVR::Environment::Environment() + : mSession(nullptr) +{ + assert(!sThis); + sThis = this; +} + +MWVR::Environment::~Environment() +{ + cleanup(); + sThis = 0; +} + +void MWVR::Environment::cleanup() +{ + if (mSession) + delete mSession; + mSession = nullptr; + if (mGUIManager) + delete mGUIManager; + mGUIManager = nullptr; + if (mViewer) + delete mViewer; + mViewer = nullptr; + if (mOpenXRManager) + delete mOpenXRManager; + mOpenXRManager = nullptr; +} + +MWVR::Environment& MWVR::Environment::get() +{ + assert(sThis); + return *sThis; +} + +MWVR::VRInputManager* MWVR::Environment::getInputManager() const +{ + auto* inputManager = MWBase::Environment::get().getInputManager(); + assert(inputManager); + auto xrInputManager = dynamic_cast(inputManager); + assert(xrInputManager); + return xrInputManager; +} + +MWVR::VRSession* MWVR::Environment::getSession() const +{ + return mSession; +} + +void MWVR::Environment::setSession(MWVR::VRSession* xrSession) +{ + mSession = xrSession; +} + +MWVR::VRGUIManager* MWVR::Environment::getGUIManager() const +{ + return mGUIManager; +} + +void MWVR::Environment::setGUIManager(MWVR::VRGUIManager* GUIManager) +{ + mGUIManager = GUIManager; +} + +MWVR::VRAnimation* MWVR::Environment::getPlayerAnimation() const +{ + return mPlayerAnimation; +} + +void MWVR::Environment::setPlayerAnimation(MWVR::VRAnimation* xrAnimation) +{ + mPlayerAnimation = xrAnimation; +} + + +MWVR::VRViewer* MWVR::Environment::getViewer() const +{ + return mViewer; +} + +void MWVR::Environment::setViewer(MWVR::VRViewer* xrViewer) +{ + mViewer = xrViewer; +} + +MWVR::OpenXRManager* MWVR::Environment::getManager() const +{ + return mOpenXRManager; +} + +void MWVR::Environment::setManager(MWVR::OpenXRManager* xrManager) +{ + mOpenXRManager = xrManager; +} + +MWVR::VRTrackingManager* MWVR::Environment::getTrackingManager() const +{ + return mTrackingManager; +} + +void MWVR::Environment::setTrackingManager(MWVR::VRTrackingManager* trackingManager) +{ + mTrackingManager = trackingManager; +} diff --git a/apps/openmw/mwvr/vrenvironment.hpp b/apps/openmw/mwvr/vrenvironment.hpp new file mode 100644 index 000000000..c62555ce2 --- /dev/null +++ b/apps/openmw/mwvr/vrenvironment.hpp @@ -0,0 +1,77 @@ +#ifndef MWVR_ENVIRONMENT_H +#define MWVR_ENVIRONMENT_H + +namespace MWVR +{ + class VRAnimation; + class VRGUIManager; + class VRInputManager; + class VRSession; + class VRTrackingManager; + class VRViewer; + class OpenXRManager; + + /// \brief Central hub for mw vr/openxr subsystems + /// + /// 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. + class Environment + { + + static Environment* sThis; + + Environment(const Environment&) = delete; + ///< not implemented + + Environment& operator= (const Environment&) = delete; + ///< not implemented + + public: + + Environment(); + + ~Environment(); + + void cleanup(); + ///< Delete all mwvr-subsystems. + + static Environment& get(); + ///< Return instance of this class. + + MWVR::VRInputManager* getInputManager() const; + + // The OpenXRInputManager supplants the regular input manager + // which is stored in MWBase::Environment + // void setInputManager(MWVR::OpenXRInputManager*); + + MWVR::VRGUIManager* getGUIManager() const; + void setGUIManager(MWVR::VRGUIManager* xrGUIManager); + + MWVR::VRAnimation* getPlayerAnimation() const; + void setPlayerAnimation(MWVR::VRAnimation* xrAnimation); + + MWVR::VRSession* getSession() const; + void setSession(MWVR::VRSession* xrSession); + + MWVR::VRViewer* getViewer() const; + void setViewer(MWVR::VRViewer* xrViewer); + + MWVR::OpenXRManager* getManager() const; + void setManager(MWVR::OpenXRManager* xrManager); + + MWVR::VRTrackingManager* getTrackingManager() const; + void setTrackingManager(MWVR::VRTrackingManager* xrManager); + + private: + MWVR::VRSession* mSession{ nullptr }; + MWVR::VRGUIManager* mGUIManager{ nullptr }; + MWVR::VRAnimation* mPlayerAnimation{ nullptr }; + MWVR::VRViewer* mViewer{ nullptr }; + MWVR::OpenXRManager* mOpenXRManager{ nullptr }; + MWVR::VRTrackingManager* mTrackingManager{ nullptr }; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrframebuffer.cpp b/apps/openmw/mwvr/vrframebuffer.cpp new file mode 100644 index 000000000..d8af935fb --- /dev/null +++ b/apps/openmw/mwvr/vrframebuffer.cpp @@ -0,0 +1,145 @@ +#include "vrframebuffer.hpp" + +#include + +#include + +#ifndef GL_TEXTURE_MAX_LEVEL +#define GL_TEXTURE_MAX_LEVEL 0x813D +#endif + +namespace MWVR +{ + + VRFramebuffer::VRFramebuffer(osg::ref_ptr state, std::size_t width, std::size_t height, uint32_t msaaSamples) + : mState(state) + , mWidth(width) + , mHeight(height) + , mDepthBuffer() + , mColorBuffer() + , mSamples(msaaSamples) + { + auto* gl = osg::GLExtensions::Get(state->getContextID(), false); + + gl->glGenFramebuffers(1, &mFBO); + + if (mSamples <= 1) + mTextureTarget = GL_TEXTURE_2D; + else + mTextureTarget = GL_TEXTURE_2D_MULTISAMPLE; + } + + void VRFramebuffer::setColorBuffer(osg::GraphicsContext* gc, uint32_t colorBuffer, bool takeOwnership) + { + auto* gl = osg::GLExtensions::Get(gc->getState()->getContextID(), false); + mColorBuffer.setTexture(colorBuffer, takeOwnership); + bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, mTextureTarget, mColorBuffer.mImage, 0); + } + + void VRFramebuffer::setDepthBuffer(osg::GraphicsContext* gc, uint32_t depthBuffer, bool takeOwnership) + { + auto* gl = osg::GLExtensions::Get(gc->getState()->getContextID(), false); + mDepthBuffer.setTexture(depthBuffer, takeOwnership); + bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + gl->glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, mTextureTarget, mDepthBuffer.mImage, 0); + } + + void VRFramebuffer::createColorBuffer(osg::GraphicsContext* gc) + { + auto colorBuffer = createImage(gc, GL_RGBA8, GL_RGBA); + setColorBuffer(gc, colorBuffer, true); + } + + void VRFramebuffer::createDepthBuffer(osg::GraphicsContext* gc) + { + auto depthBuffer = createImage(gc, GL_DEPTH24_STENCIL8_EXT, GL_DEPTH_COMPONENT); + setDepthBuffer(gc, depthBuffer, true); + } + + VRFramebuffer::~VRFramebuffer() + { + destroy(nullptr); + } + + void VRFramebuffer::destroy(osg::State* state) + { + if (!state) + { + // Try re-using the state received during construction + state = mState.get(); + } + + if (state) + { + auto* gl = osg::GLExtensions::Get(state->getContextID(), false); + if (mFBO) + gl->glDeleteFramebuffers(1, &mFBO); + } + else if (mFBO) + // Without access to glDeleteFramebuffers, i'll have to leak FBOs. + Log(Debug::Warning) << "destroy() called without a State. Leaking FBO"; + mFBO = 0; + + mColorBuffer.delet(); + mDepthBuffer.delet(); + } + + void VRFramebuffer::bindFramebuffer(osg::GraphicsContext* gc, uint32_t target) + { + auto state = gc->getState(); + auto* gl = osg::GLExtensions::Get(state->getContextID(), false); + //if (gl->glCheckFramebufferStatus(GL_FRAMEBUFFER_EXT) != GL_FRAMEBUFFER_COMPLETE_EXT) + // throw std::runtime_error("Tried to bind incomplete framebuffer"); + gl->glBindFramebuffer(target, mFBO); + } + + void VRFramebuffer::blit(osg::GraphicsContext* gc, int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, uint32_t bits, uint32_t filter) + { + auto* state = gc->getState(); + auto* gl = osg::GLExtensions::Get(state->getContextID(), false); + gl->glBindFramebuffer(GL_READ_FRAMEBUFFER_EXT, mFBO); + gl->glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, bits, filter); + gl->glBindFramebuffer(GL_READ_FRAMEBUFFER_EXT, 0); + } + + uint32_t VRFramebuffer::createImage(osg::GraphicsContext* gc, uint32_t formatInternal, uint32_t format) + { + auto* gl = osg::GLExtensions::Get(gc->getState()->getContextID(), false); + uint32_t image; + glGenTextures(1, &image); + glBindTexture(mTextureTarget, image); + if (mSamples <= 1) + { + glTexImage2D(mTextureTarget, 0, formatInternal, mWidth, mHeight, 0, format, GL_UNSIGNED_INT, nullptr); + glTexParameteri(mTextureTarget, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER_ARB); + glTexParameteri(mTextureTarget, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER_ARB); + glTexParameteri(mTextureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(mTextureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } + else + gl->glTexImage2DMultisample(mTextureTarget, mSamples, format, mWidth, mHeight, false); + + glTexParameteri(mTextureTarget, GL_TEXTURE_MAX_LEVEL, 0); + + return image; + } + + void VRFramebuffer::Texture::delet() + { + if (mOwner) + glDeleteTextures(1, &mImage); + + mImage = 0; + mOwner = false; + } + + void VRFramebuffer::Texture::setTexture(uint32_t image, bool owner) + { + if (mImage) + delet(); + + mImage = image; + mOwner = owner; + } +} diff --git a/apps/openmw/mwvr/vrframebuffer.hpp b/apps/openmw/mwvr/vrframebuffer.hpp new file mode 100644 index 000000000..913af37b2 --- /dev/null +++ b/apps/openmw/mwvr/vrframebuffer.hpp @@ -0,0 +1,70 @@ +#ifndef MWVR_OPENRXTEXTURE_H +#define MWVR_OPENRXTEXTURE_H + +#include +#include +#include +#include + +#include "openxrmanager.hpp" + +namespace MWVR +{ + /// \brief Manages an opengl framebuffer + /// + /// Intended for managing the vr swapchain, but is also use to manage the mirror texture as a convenience. + class VRFramebuffer + { + private: + struct Texture + { + uint32_t mImage = 0; + bool mOwner = false; + + void delet(); + void setTexture(uint32_t image, bool owner); + }; + + public: + VRFramebuffer(osg::ref_ptr state, std::size_t width, std::size_t height, uint32_t msaaSamples); + ~VRFramebuffer(); + + void destroy(osg::State* state); + + auto width() const { return mWidth; } + auto height() const { return mHeight; } + auto msaaSamples() const { return mSamples; } + + void bindFramebuffer(osg::GraphicsContext* gc, uint32_t target); + + void setColorBuffer(osg::GraphicsContext* gc, uint32_t colorBuffer, bool takeOwnership); + void setDepthBuffer(osg::GraphicsContext* gc, uint32_t depthBuffer, bool takeOwnership); + void createColorBuffer(osg::GraphicsContext* gc); + void createDepthBuffer(osg::GraphicsContext* gc); + + //! ref glBlitFramebuffer + void blit(osg::GraphicsContext* gc, int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, uint32_t bits, uint32_t filter = GL_LINEAR); + + uint32_t colorBuffer() const { return mColorBuffer.mImage; }; + uint32_t depthBuffer() const { return mDepthBuffer.mImage; }; + + private: + uint32_t createImage(osg::GraphicsContext* gc, uint32_t formatInternal, uint32_t format); + + // Set aside a weak pointer to the constructor state to use when freeing FBOs, if no state is given to destroy() + osg::observer_ptr mState; + + // Metadata + std::size_t mWidth = 0; + std::size_t mHeight = 0; + + // Render Target + uint32_t mFBO = 0; + Texture mDepthBuffer; + Texture mColorBuffer; + uint32_t mSamples = 0; + uint32_t mTextureTarget = 0; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrgui.cpp b/apps/openmw/mwvr/vrgui.cpp new file mode 100644 index 000000000..5299ed3f7 --- /dev/null +++ b/apps/openmw/mwvr/vrgui.cpp @@ -0,0 +1,1030 @@ +#include "vrgui.hpp" + +#include + +#include "vranimation.hpp" +#include "vrenvironment.hpp" +#include "vrpointer.hpp" +#include "vrsession.hpp" +#include "openxrinput.hpp" +#include "openxrmanagerimpl.hpp" + +#include +#include +#include +#include +#include +#include +#include + + +#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 +#include +#include +#include +#include +#include +#include +#include + +namespace osg +{ + // 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 * 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) + { + 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); + setCullMaskLeft(MWRender::Mask_GUI); + setCullMaskRight(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 * Constants::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] = bottom_left; + (*vertices)[1] = top_left; + (*vertices)[2] = bottom_right; + (*vertices)[3] = top_right; + mGeometry->setVertexArray(vertices); + (*texCoords)[0].set(0.0f, 0.0f); + (*texCoords)[1].set(0.0f, 1.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); + // TODO: Just use GL_TRIANGLES + mGeometry->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLE_STRIP, 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); + mGUICamera->setScene(mMyGUICamera); + + // Define state set that allows rendering with transparency + osg::StateSet* stateSet = mGeometry->getOrCreateStateSet(); + auto texture = menuTexture(); + texture->setName("diffuseMap"); + stateSet->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); + + osg::ref_ptr mat = new osg::Material; + mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); + stateSet->setAttribute(mat); + + // 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 * static_cast(mConfig.priority); + } + + //mTransform->addUpdateCallback(new LayerUpdateCallback(this)); + + auto* tm = Environment::get().getTrackingManager(); + mTrackingPath = tm->stringToVRPath(mConfig.trackingPath); + tm->bind(this, "uisource"); + } + + 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::onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) + { + auto tp = source.getTrackingPose(predictedDisplayTime, mTrackingPath); + if (!!tp.status) + { + mTrackedPose = tp.pose; + updatePose(); + } + + update(); + } + + void VRGUILayer::updatePose() + { + + auto orientation = mRotation * mTrackedPose.orientation; + + if(mLayerName == "StatusHUD" || mLayerName == "VirtualKeyboard") + { + orientation = osg::Quat(osg::PI_2, osg::Vec3(0, 0, 1)) * orientation; + } + + // Orient the offset and move the layer + auto position = mTrackedPose.position + orientation * mConfig.offset * Constants::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 i just stretch the rectangle to the bottom. + mRealRect.bottom = 1.f; + } + } + + void VRGUILayer::update() + { + 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) / Constants::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) * static_cast(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.; + + if(mLayerName == "InputBlocker") + mMyGUICamera->setProjectionMatrixAsOrtho2D(menuRight, menuLeft, menuTop, menuBottom); + else + 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; + } + } + } + + class VRGUIManagerUpdateCallback : public osg::Callback + { + public: + VRGUIManagerUpdateCallback(VRGUIManager* manager) + : mManager(manager) + { + + } + + bool run(osg::Object* object, osg::Object* data) + { + mManager->update(); + return traverse(object, data); + } + + private: + VRGUIManager* mManager; + }; + + static const LayerConfig createDefaultConfig(int priority, bool background = true, SizingMode sizingMode = SizingMode::Auto, std::string extraLayers = "Popup") + { + 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, + "/ui/input/stationary/pose", + extraLayers + }; + } + + static const float sSideBySideRadius = 1.f; + static const float sSideBySideAzimuthInterval = -osg::PI_4; + + static const LayerConfig createSideBySideConfig(int priority) + { + LayerConfig config = createDefaultConfig(priority, true, SizingMode::Fixed, ""); + config.sideBySide = true; + config.offset = osg::Vec3(0.f, sSideBySideRadius, -.25f); + config.extent = radiusAngleWidth(sSideBySideRadius, sSideBySideAzimuthInterval); + config.myGUIViewSize = osg::Vec2(0.70f, 0.70f); + return config; + }; + + static osg::Vec3 gLeftHudOffsetTop = osg::Vec3(-0.200f, -.05f, .066f); + static osg::Vec3 gLeftHudOffsetWrist = osg::Vec3(-0.200f, -.090f, -.033f); + + void VRGUIManager::setGeometryRoot(osg::Group* root) + { + mGeometriesRootNode->removeChild(mGeometries); + mGeometriesRootNode = root; + mGeometriesRootNode->addChild(mGeometries); + } + + void VRGUIManager::setCameraRoot(osg::Group* root) + { + mGUICamerasRootNode->removeChild(mGUICameras); + mGUICamerasRootNode = root; + mGUICamerasRootNode->addChild(mGUICameras); + } + + VRGUIManager::VRGUIManager( + osg::ref_ptr viewer, + Resource::ResourceSystem* resourceSystem, + osg::Group* rootNode) + : mOsgViewer(viewer) + , mResourceSystem(resourceSystem) + , mGeometriesRootNode(rootNode) + , mGUICamerasRootNode(rootNode) + , mUiTracking(new VRGUITracking("pcworld")) + { + mGeometries->setName("VR GUI Geometry Root"); + mGeometries->setUpdateCallback(new VRGUIManagerUpdateCallback(this)); + mGeometries->setNodeMask(MWRender::VisMask::Mask_3DGUI); + mGeometriesRootNode->addChild(mGeometries); + + auto stateSet = mGeometries->getOrCreateStateSet(); + 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); + // assign large value to effectively turn off fog + // shaders don't respect glDisable(GL_FOG) + osg::ref_ptr fog(new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + stateSet->setAttributeAndModes(fog, osg::StateAttribute::OFF); + + osg::ref_ptr lightmodel = new osg::LightModel; + lightmodel->setAmbientIntensity(osg::Vec4(1.0, 1.0, 1.0, 1.0)); + stateSet->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); + + SceneUtil::ShadowManager::disableShadowsForStateSet(stateSet); + mGeometries->setStateSet(stateSet); + + mGUICameras->setName("VR GUI Cameras Root"); + mGUICameras->setNodeMask(MWRender::VisMask::Mask_3DGUI); + mGUICamerasRootNode->addChild(mGUICameras); + + LayerConfig defaultConfig = createDefaultConfig(1); + LayerConfig loadingScreenConfig = createDefaultConfig(1, true, SizingMode::Fixed, "Menu"); + LayerConfig mainMenuConfig = createDefaultConfig(1, true); + LayerConfig journalBooksConfig = createDefaultConfig(2, false, SizingMode::Fixed); + LayerConfig defaultWindowsConfig = createDefaultConfig(3, true); + LayerConfig videoPlayerConfig = createDefaultConfig(4, true, SizingMode::Fixed); + LayerConfig messageBoxConfig = createDefaultConfig(6, false, SizingMode::Auto);; + LayerConfig notificationConfig = createDefaultConfig(7, false, SizingMode::Fixed); + LayerConfig listBoxConfig = createDefaultConfig(10, true); + + LayerConfig statsWindowConfig = createSideBySideConfig(0); + LayerConfig inventoryWindowConfig = createSideBySideConfig(1); + LayerConfig spellWindowConfig = createSideBySideConfig(2); + LayerConfig mapWindowConfig = createSideBySideConfig(3); + LayerConfig inventoryCompanionWindowConfig = createSideBySideConfig(4); + LayerConfig dialogueWindowConfig = createSideBySideConfig(5); + + osg::Vec3 leftHudOffset = gLeftHudOffsetWrist; + + std::string leftHudSetting = Settings::Manager::getString("left hand hud position", "VR"); + if (Misc::StringUtils::ciEqual(leftHudSetting, "top")) + leftHudOffset = gLeftHudOffsetTop; + + osg::Vec3 vkeyboardOffset = leftHudOffset + osg::Vec3(0,0.0001,0); + + LayerConfig virtualKeyboardConfig = LayerConfig{ + 10, + false, + osg::Vec4{0.f,0.f,0.f,.75f}, + vkeyboardOffset, // offset (meters) + osg::Vec2(0.f,0.5f), // center (model space) + osg::Vec2(.25f, .25f), // extent (meters) + 2048, // Spatial resolution (pixels per meter) + osg::Vec2i(2048,2048), // Texture resolution + osg::Vec2(1,1), + SizingMode::Auto, + "/user/hand/left/input/aim/pose", + "" + }; + LayerConfig statusHUDConfig = LayerConfig + { + 0, + false, // side-by-side + osg::Vec4{}, // background + leftHudOffset, // 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), + defaultConfig.myGUIViewSize, + SizingMode::Auto, + "/user/hand/left/input/aim/pose", + "" + }; + + LayerConfig popupConfig = LayerConfig + { + 0, + false, // side-by-side + osg::Vec4{0.f,0.f,0.f,0.f}, // background + osg::Vec3(-0.025f,-.200f,.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), + defaultConfig.myGUIViewSize, + SizingMode::Auto, + "/user/hand/right/input/aim/pose", + "" + }; + + mLayerConfigs = std::map + { + {"DefaultConfig", defaultConfig}, + {"StatusHUD", statusHUDConfig}, + {"Tooltip", popupConfig}, + {"JournalBooks", journalBooksConfig}, + {"InventoryCompanionWindow", inventoryCompanionWindowConfig}, + {"InventoryWindow", inventoryWindowConfig}, + {"SpellWindow", spellWindowConfig}, + {"MapWindow", mapWindowConfig}, + {"StatsWindow", statsWindowConfig}, + {"DialogueWindow", dialogueWindowConfig}, + {"MessageBox", messageBoxConfig}, + {"Windows", defaultWindowsConfig}, + {"ListBox", listBoxConfig}, + {"MainMenu", mainMenuConfig}, + {"Notification", notificationConfig}, + {"InputBlocker", videoPlayerConfig}, + {"Menu", videoPlayerConfig}, + {"LoadingScreen", loadingScreenConfig}, + {"VirtualKeyboard", virtualKeyboardConfig}, + }; + } + + VRGUIManager::~VRGUIManager(void) + { + } + + 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 * static_cast(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{}; + auto configIt = mLayerConfigs.find(name); + if (configIt != mLayerConfigs.end()) + { + config = configIt->second; + } + else + { + Log(Debug::Warning) << "Layer " << name << " has no configuration, using default"; + config = mLayerConfigs["DefaultConfig"]; + } + + auto layer = std::shared_ptr(new VRGUILayer( + mGeometries, + mGUICameras, + name, + config, + this + )); + mLayers[name] = layer; + + layer->mGeometry->setUserData(new VRGUILayerUserData(mLayers[name])); + + if (config.sideBySide) + { + mSideBySideLayers.push_back(layer); + updateSideBySideLayers(); + } + + Resource::SceneManager* sceneManager = mResourceSystem->getSceneManager(); + sceneManager->recreateShaders(layer->mGeometry); + } + + void VRGUIManager::insertWidget(MWGui::Layout* widget) + { + auto* layer = widget->mMainWidget->getLayer(); + auto name = layer->getName(); + + auto it = mLayers.find(name); + if (it == mLayers.end()) + { + insertLayer(name); + it = mLayers.find(name); + if (it == mLayers.end()) + { + Log(Debug::Error) << "Failed to insert layer " << name; + return; + } + } + + it->second->insertWidget(widget); + + if (it->second.get() != mFocusLayer) + setPick(widget, 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++) + { + 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()) + { + return; + } + + 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(); + + if (layerBlacklist.find(name) != layerBlacklist.end()) + { + // Never pick an invisible layer + setPick(widget, false); + return; + } + + if (visible) + insertWidget(widget); + else + removeWidget(widget); + } + + void VRGUIManager::updateTracking() + { + mUiTracking->resetStationaryPose(); + } + + bool VRGUIManager::updateFocus() + { + auto* world = MWBase::Environment::get().getWorld(); + if (world) + { + auto& pointer = world->getUserPointer(); + if (pointer.getPointerTarget().mHit) + { + std::shared_ptr newFocusLayer = nullptr; + auto* node = pointer.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(pointer.getPointerTarget().mHitPointLocal); + return true; + } + } + } + return false; + } + + void VRGUIManager::update() + { + auto xr = MWVR::Environment::get().getManager(); + if (xr) + if (!xr->appShouldRender()) + updateTracking(); + } + + void VRGUIManager::setFocusLayer(VRGUILayer* layer) + { + if (layer == mFocusLayer) + return; + + if (mFocusLayer) + { + if (!mFocusLayer->mWidgets.empty()) + setPick(mFocusLayer->mWidgets.front(), false); + } + mFocusLayer = layer; + if (mFocusLayer) + { + if (!mFocusLayer->mWidgets.empty()) + { + Log(Debug::Verbose) << "Set focus layer to " << mFocusLayer->mWidgets.front()->mMainWidget->getLayer()->getName(); + setPick(mFocusLayer->mWidgets.front(), true); + } + } + else + { + Log(Debug::Verbose) << "Set focus layer to null"; + } + } + + void VRGUIManager::setFocusWidget(MyGUI::Widget* widget) + { + // TODO: This relies on MyGUI internal functions and may break on any future version. + if (widget == mFocusWidget) + return; + if (mFocusWidget) + mFocusWidget->_riseMouseLostFocus(widget); + if (widget) + widget->_riseMouseSetFocus(mFocusWidget); + mFocusWidget = widget; + } + + void VRGUIManager::configUpdated(const std::string& layer) + { + auto it = mLayers.find(layer); + if (it != mLayers.end()) + { + it->second->mConfig = mLayerConfigs[layer]; + } + } + + void VRGUIManager::notifyWidgetUnlinked(MyGUI::Widget* widget) + { + if (widget == mFocusWidget) + mFocusWidget = nullptr; + } + + bool VRGUIManager::injectMouseClick(bool onPress) + { + // TODO: This relies on a MyGUI internal functions and may break un any future version. + if (mFocusWidget) + { + if(onPress) + mFocusWidget->_riseMouseButtonClick(); + return true; + } + return false; + } + + void VRGUIManager::processChangedSettings(const std::set>& changed) + { + for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) + { + if (it->first == "VR" && it->second == "left hand hud position") + { + std::string leftHudSetting = Settings::Manager::getString("left hand hud position", "VR"); + if (Misc::StringUtils::ciEqual(leftHudSetting, "top")) + mLayerConfigs["StatusHUD"].offset = gLeftHudOffsetTop; + else + mLayerConfigs["StatusHUD"].offset = gLeftHudOffsetWrist; + mLayerConfigs["VirtualKeyboard"].offset = mLayerConfigs["StatusHUD"].offset + osg::Vec3(0,0.0001,0); + + configUpdated("StatusHUD"); + configUpdated("VirtualKeyboard"); + } + } + } + + class Pickable + { + public: + virtual void setPick(bool pick) = 0; + }; + + template + class PickLayer : public L, public Pickable + { + public: + using L::L; + + void setPick(bool pick) override + { + L::mIsPick = pick; + } + }; + + template + class MyFactory + { + public: + using LayerType = L; + using PickLayerType = PickLayer; + using Delegate = MyGUI::delegates::CDelegate1; + static typename Delegate::IDelegate* getFactory() + { + return MyGUI::newDelegate(createFromFactory); + } + + static void registerFactory() + { + MyGUI::FactoryManager::getInstance().registerFactory("Layer", LayerType::getClassTypeName(), getFactory()); + } + + private: + static void createFromFactory(MyGUI::IObject*& _instance) + { + _instance = new PickLayerType(); + } + }; + + void VRGUIManager::registerMyGUIFactories() + { + MyFactory< MyGUI::OverlappedLayer >::registerFactory(); + MyFactory< MyGUI::SharedLayer >::registerFactory(); + MyFactory< osgMyGUI::AdditiveLayer >::registerFactory(); + MyFactory< osgMyGUI::AdditiveLayer >::registerFactory(); + } + + void VRGUIManager::setPick(MWGui::Layout* widget, bool pick) + { + auto* layer = widget->mMainWidget->getLayer(); + auto* pickable = dynamic_cast(layer); + if (pickable) + pickable->setPick(pick); + } + + 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(); + float width = static_cast(viewSize.width) * rect.width(); + float height = static_cast(viewSize.height) * rect.height(); + float left = static_cast(viewSize.width) * rect.left; + float bottom = static_cast(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); + + // The virtual keyboard must be interactive regardless of modals + // This could be generalized with another config entry, but i don't think any other + // widgets/layers need it so i'm hardcoding it for the VirtualKeyboard for now. + if ( + mFocusLayer + && mFocusLayer->mLayerName == "VirtualKeyboard" + && MyGUI::InputManager::getInstance().isModalAny()) + { + auto* widget = MyGUI::LayerManager::getInstance().getWidgetFromPoint((int)x, (int)y); + setFocusWidget(widget); + } + else + setFocusWidget(nullptr); + + } + + VRGUITracking::VRGUITracking(const std::string& source) + : VRTrackingSource("uisource") + { + auto* tm = Environment::get().getTrackingManager(); + mSource = tm->getSource(source); + mHeadPath = tm->stringToVRPath("/user/head/input/pose"); + mStationaryPath = tm->stringToVRPath("/ui/input/stationary/pose"); + } + + VRTrackingPose VRGUITracking::getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath reference) + { + if (path == mStationaryPath) + return mStationaryPose; + return mSource->getTrackingPose(predictedDisplayTime, path, reference); + } + + std::vector VRGUITracking::listSupportedTrackingPosePaths() const + { + auto paths = mSource->listSupportedTrackingPosePaths(); + paths.push_back(mStationaryPath); + return paths; + } + + void VRGUITracking::updateTracking(DisplayTime predictedDisplayTime) + { + if (mSource->availablePosesChanged()) + notifyAvailablePosesChanged(); + + if (mShouldUpdateStationaryPose) + { + auto tp = mSource->getTrackingPose(predictedDisplayTime, mHeadPath); + if (!!tp.status) + { + mShouldUpdateStationaryPose = false; + mStationaryPose = tp; + + // Stationary UI elements should always be vertical + auto axis = osg::Z_AXIS; + osg::Quat vertical; + auto local = mStationaryPose.pose.orientation * axis; + vertical.makeRotate(local, axis); + mStationaryPose.pose.orientation = mStationaryPose.pose.orientation * vertical; + } + } + } + + void VRGUITracking::resetStationaryPose() + { + mShouldUpdateStationaryPose = true; + } + +} diff --git a/apps/openmw/mwvr/vrgui.hpp b/apps/openmw/mwvr/vrgui.hpp new file mode 100644 index 000000000..26823c518 --- /dev/null +++ b/apps/openmw/mwvr/vrgui.hpp @@ -0,0 +1,232 @@ +#ifndef OPENXR_MENU_HPP +#define OPENXR_MENU_HPP + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "openxrmanager.hpp" +#include "vrtracking.hpp" + +namespace MyGUI +{ + class Widget; + class Window; +} + +namespace MWGui +{ + class Layout; + class WindowBase; +} + +namespace Resource +{ + class ResourceSystem; +} + +struct XrCompositionLayerQuad; +namespace MWVR +{ + class GUICamera; + class VRGUIManager; + + // Some UI elements should occupy predefined geometries + // Others should grow/shrink freely + enum class SizingMode + { + Auto, + Fixed + }; + + /// Configuration of a VRGUILayer + struct LayerConfig + { + int priority; //!< Higher priority shows over lower priority windows by moving higher priority layers slightly closer to the player. + bool sideBySide; //!< All layers with this config will show up side by side in a partial circle around the player, and will all be resized to a predefined size. + osg::Vec4 backgroundColor; //!< Background color of layer + osg::Vec3 offset; //!< Offset from tracked node in meters + osg::Vec2 center; //!< Model space centerpoint of menu geometry. All menu geometries have model space lengths of 1 in each dimension. Use this to affect how geometries grow with changing size. + osg::Vec2 extent; //!< Spatial extent of the layer in meters when using Fixed sizing mode + int spatialResolution; //!< Pixels when using the Auto sizing mode. \note Pixels per meter of the GUI viewport, not the RTT texture. + osg::Vec2i pixelResolution; //!< Pixel resolution of the RTT texture + osg::Vec2 myGUIViewSize; //!< Resizable elements are resized to this (fraction of full view) + SizingMode sizingMode; //!< How to size the layer + std::string trackingPath; //!< The path that will be used to read tracking data + std::string extraLayers; //!< Additional layers to draw (list separated by any non-alphabetic) + + bool operator<(const LayerConfig& rhs) const { return priority < rhs.priority; } + }; + + //! Extends the tracking source with /ui/input/stationary/pose + //! \note reference space will be ignored when reading /ui/input/stationary/pose + class VRGUITracking : public VRTrackingSource + { + public: + VRGUITracking(const std::string& source); + + virtual std::vector listSupportedTrackingPosePaths() const override; + virtual void updateTracking(DisplayTime predictedDisplayTime) override; + void resetStationaryPose(); + + protected: + virtual VRTrackingPose getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath reference = 0) override; + + private: + VRPath mStationaryPath = 0; + VRPath mHeadPath = 0; + VRTrackingPose mStationaryPose = VRTrackingPose(); + VRTrackingSource* mSource = nullptr; + + bool mShouldUpdateStationaryPose = true; + }; + + /// \brief A single VR GUI Quad. + /// + /// In VR menus are shown as quads within the game world. + /// The behaviour of that quad is defined by the MWVR::LayerConfig struct + /// Each instance of VRGUILayer is used to show one MYGUI layer. + class VRGUILayer : public VRTrackingListener + { + public: + VRGUILayer( + osg::ref_ptr geometryRoot, + osg::ref_ptr cameraRoot, + std::string layerName, + LayerConfig config, + VRGUIManager* parent); + ~VRGUILayer(); + + void update(); + + protected: + friend class VRGUIManager; + osg::Camera* camera(); + osg::ref_ptr menuTexture(); + void setAngle(float angle); + void updatePose(); + void updateRect(); + void insertWidget(MWGui::Layout* widget); + void removeWidget(MWGui::Layout* widget); + int widgetCount() { return mWidgets.size(); } + bool operator<(const VRGUILayer& rhs) const { return mConfig.priority < rhs.mConfig.priority; } + + /// Update layer quads based on current tracking information + void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) override; + + public: + VRPath mTrackingPath = 0; + Pose mTrackingPose = Pose(); + + Pose mTrackedPose{}; + LayerConfig mConfig; + std::string mLayerName; + std::vector mWidgets; + osg::ref_ptr mGeometryRoot; + osg::ref_ptr mGeometry{ new osg::Geometry }; + osg::ref_ptr mTransform{ new osg::PositionAttitudeTransform }; + osg::ref_ptr mCameraRoot; + osg::ref_ptr mGUICamera; + osg::ref_ptr mMyGUICamera{ nullptr }; + MyGUI::FloatRect mRealRect{}; + osg::Quat mRotation{ 0,0,0,1 }; + }; + + /// \brief osg user data used to refer back to VRGUILayer objects when intersecting with the scene graph. + class VRGUILayerUserData : public osg::Referenced + { + public: + VRGUILayerUserData(std::shared_ptr layer) : mLayer(layer) {}; + + std::weak_ptr mLayer; + }; + + /// \brief Manager of VRGUILayer objects. + /// + /// Constructs and destructs VRGUILayer objects in response to MWGui::Layout::setVisible calls. + /// Layers can also be made visible directly by calling insertLayer() directly, e.g. to show + /// the video player. + class VRGUIManager : public VRTrackingListener + { + public: + VRGUIManager( + osg::ref_ptr viewer, + Resource::ResourceSystem* resourceSystem, + osg::Group* rootNode); + + ~VRGUIManager(void); + + /// Set visibility of the layer this layout is on + void setVisible(MWGui::Layout*, bool visible); + + /// Insert the given layer quad if it isn't already + void insertLayer(const std::string& name); + + /// Remove the given layer quad + void removeLayer(const std::string& name); + + /// Check current pointer target and update focus layer + bool updateFocus(); + + /// Update traversal + void update(); + + /// Update traversal + void updateTracking(); + + /// Gui cursor coordinates to use to simulate a mouse press/move if the player is currently pointing at a vr gui layer + osg::Vec2i guiCursor() { return mGuiCursor; }; + + /// Inject mouse click if applicable + bool injectMouseClick(bool onPress); + + /// Notify that widget is about to be destroyed. + void notifyWidgetUnlinked(MyGUI::Widget* widget); + + /// Update settings where applicable + void processChangedSettings(const std::set< std::pair >& changed); + + static void registerMyGUIFactories(); + + static void setPick(MWGui::Layout* widget, bool pick); + + void setGeometryRoot(osg::Group* root); + void setCameraRoot(osg::Group* root); + + private: + void computeGuiCursor(osg::Vec3 hitPoint); + void updateSideBySideLayers(); + void insertWidget(MWGui::Layout* widget); + void removeWidget(MWGui::Layout* widget); + void setFocusLayer(VRGUILayer* layer); + void setFocusWidget(MyGUI::Widget* widget); + void configUpdated(const std::string& layer); + + osg::ref_ptr mOsgViewer{ nullptr }; + Resource::ResourceSystem* mResourceSystem; + + osg::ref_ptr mGeometriesRootNode{ nullptr }; + osg::ref_ptr mGeometries{ new osg::Group }; + osg::ref_ptr mGUICamerasRootNode{ nullptr }; + osg::ref_ptr mGUICameras{ new osg::Group }; + + std::unique_ptr mUiTracking = nullptr; + + std::map> mLayers; + std::vector > mSideBySideLayers; + + osg::Vec2i mGuiCursor{}; + VRGUILayer* mFocusLayer{ nullptr }; + MyGUI::Widget* mFocusWidget{ nullptr }; + std::map mLayerConfigs{}; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrinput.cpp b/apps/openmw/mwvr/vrinput.cpp new file mode 100644 index 000000000..cd3ecc636 --- /dev/null +++ b/apps/openmw/mwvr/vrinput.cpp @@ -0,0 +1,174 @@ +#include "vrinput.hpp" +#include "openxrdebug.hpp" +#include "vrenvironment.hpp" +#include "openxrmanagerimpl.hpp" +#include "openxrtypeconversions.hpp" + +#include +#include +#include +#include +#include + +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 }; + + 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)); + VrDebug::setName(mXRSpace, "OpenMW XR Action Space " + mXRAction->mName); + } + + 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()) + { + 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; + } + + + AxisAction::AxisAction(int openMWAction, std::unique_ptr xrAction, std::shared_ptr deadzone) + : Action(openMWAction, std::move(xrAction)) + , mDeadzone(deadzone) + { + } + + void AxisAction::update() + { + mActive = false; + mXRAction->getFloat(0, mValue); + mDeadzone->applyDeadzone(mValue); + + if (std::fabs(mValue) > gAxisEpsilon) + mActive = true; + else + mValue = 0.f; + } + + void AxisAction::Deadzone::applyDeadzone(float& value) + { + float sign = std::copysignf(1.f, value); + float magnitude = std::fabs(value); + magnitude = std::min(mActiveRadiusOuter, magnitude); + magnitude = std::max(0.f, magnitude - mActiveRadiusInner); + value = sign * magnitude * mActiveScale; + + } + + void AxisAction::Deadzone::setDeadzoneRadius(float deadzoneRadius) + { + deadzoneRadius = std::min(std::max(deadzoneRadius, 0.0f), 0.5f - 1e-5f); + mActiveRadiusInner = deadzoneRadius; + mActiveRadiusOuter = 1.f - deadzoneRadius; + float activeRadius = mActiveRadiusOuter - mActiveRadiusInner; + assert(activeRadius > 0.f); + mActiveScale = 1.f / activeRadius; + } + +} diff --git a/apps/openmw/mwvr/vrinput.hpp b/apps/openmw/mwvr/vrinput.hpp new file mode 100644 index 000000000..b515193df --- /dev/null +++ b/apps/openmw/mwvr/vrinput.hpp @@ -0,0 +1,236 @@ +#ifndef VR_INPUT_HPP +#define VR_INPUT_HPP + +#include "vrviewer.hpp" +#include "openxraction.hpp" + +#include "../mwinput/inputmanagerimp.hpp" + +namespace MWVR +{ + /// Extension of MWInput's set of actions. + enum VrActions + { + A_VrFirst = MWInput::A_Last + 1, + A_VrMetaMenu, + A_ActivateTouch, + A_HapticsLeft, + A_HapticsRight, + A_HandPoseLeft, + A_HandPoseRight, + A_MenuUpDown, + A_MenuLeftRight, + A_MenuSelect, + A_MenuBack, + A_Recenter, + A_VrLast + }; + + enum class VrControlType + { + Press, + LongPress, + Hold, + Pose, + Haptic, + Axis + }; + + /// \brief Suggest a binding by binding an action to a path on a given hand (left or right). + struct SuggestedBinding + { + std::string path; + std::string action; + }; + + using SuggestedBindings = std::vector; + + /// \brief Enumeration of action sets + enum class ActionSet + { + GUI = 0, + Gameplay = 1, + Tracking = 2, + Haptics = 3, + }; + + /// \brief Action for applying haptics + class HapticsAction + { + public: + HapticsAction(std::unique_ptr xrAction) : mXRAction{ std::move(xrAction) } {}; + + //! Apply vibration at the given amplitude + void apply(float amplitude); + + //! Convenience + operator XrAction() { return *mXRAction; } + + private: + std::unique_ptr mXRAction; + float mAmplitude{ 0.f }; + }; + + /// \brief Action for capturing tracking information + class PoseAction + { + public: + PoseAction(std::unique_ptr xrAction); + + //! Current value of an axis or lever action + Pose value() const { return mValue; } + + //! Previous value + Pose previousValue() const { return mPrevious; } + + //! Convenience + operator XrAction() { return *mXRAction; } + + //! Update pose value + void update(long long time); + + //! Action space + XrSpace xrSpace() { return mXRSpace; } + + private: + std::unique_ptr mXRAction; + XrSpace mXRSpace; + Pose mValue{}; + Pose mPrevious{}; + }; + + /// \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 action changed to being released in the last update + bool isActive() const { return mActive; }; + + //! True if activation turned on this update (i.e. onPress) + bool onActivate() const { return mOnActivate; } + + //! True if activation turned off this update (i.e. onRelease) + bool onDeactivate() const { return mOnDeactivate; } + + //! OpenMW Action code of this action + int openMWActionCode() const { return mOpenMWAction; } + + //! Current value of an axis or lever action + float value() const { return mValue; } + + //! Previous value + float previousValue() const { return mPrevious; } + + //! Update internal states. Note that subclasses maintain both mValue and mActivate to allow + //! axis and press to subtitute one another. + virtual void update() = 0; + + //! Determines if an action update should be queued + virtual bool shouldQueue() const = 0; + + //! Convenience + operator XrAction() { return *mXRAction; } + + //! Update and queue action if applicable + void updateAndQueue(std::deque& queue); + + protected: + 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; + + static const XrActionType ActionType = XR_ACTION_TYPE_BOOLEAN_INPUT; + + void update() override; + + 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{}; + }; + + //! 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; + + void update() override; + + 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{}; + }; + + //! 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; + + void update() override; + + virtual bool shouldQueue() const override { return mActive || onDeactivate(); } + + 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: + class Deadzone + { + public: + void applyDeadzone(float& value); + + void setDeadzoneRadius(float deadzoneRadius); + + private: + float mActiveRadiusInner{ 0.f }; + float mActiveRadiusOuter{ 1.f }; + float mActiveScale{ 1.f }; + }; + + public: + AxisAction(int openMWAction, std::unique_ptr xrAction, std::shared_ptr deadzone); + + static const XrActionType ActionType = XR_ACTION_TYPE_FLOAT_INPUT; + + void update() override; + + virtual bool shouldQueue() const override { return mActive || onDeactivate(); } + + std::shared_ptr mDeadzone; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrinputmanager.cpp b/apps/openmw/mwvr/vrinputmanager.cpp new file mode 100644 index 000000000..dd38c39ca --- /dev/null +++ b/apps/openmw/mwvr/vrinputmanager.cpp @@ -0,0 +1,772 @@ +#include "vrinputmanager.hpp" + +#include "vranimation.hpp" +#include "vrcamera.hpp" +#include "vrenvironment.hpp" +#include "vrgui.hpp" +#include "vrpointer.hpp" +#include "vrviewer.hpp" +#include "openxrinput.hpp" +#include "openxrmanager.hpp" +#include "openxrmanagerimpl.hpp" +#include "openxraction.hpp" +#include "realisticcombat.hpp" + +#include + +#include + +#include "../mwbase/world.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/statemanager.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" + +#include "../mwgui/draganddrop.hpp" +#include "../mwgui/inventorywindow.hpp" + +#include "../mwinput/actionmanager.hpp" +#include "../mwinput/bindingsmanager.hpp" +#include "../mwinput/mousemanager.hpp" +#include "../mwinput/sdlmappings.hpp" + +#include "../mwworld/player.hpp" +#include "../mwmechanics/actorutil.hpp" + +#include "../mwrender/renderingmanager.hpp" +#include "../mwrender/camera.hpp" + +#include +#include + +#include + +namespace MWVR +{ + + Pose VRInputManager::getLimbPose(int64_t time, TrackedLimb limb) + { + return mXRInput->getActionSet(ActionSet::Tracking).getLimbPose(time, limb); + } + + OpenXRActionSet& VRInputManager::activeActionSet() + { + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + guiMode = guiMode || (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame); + if (guiMode) + { + return mXRInput->getActionSet(ActionSet::GUI); + } + return mXRInput->getActionSet(ActionSet::Gameplay); + } + + void VRInputManager::notifyInteractionProfileChanged() + { + mXRInput->notifyInteractionProfileChanged(); + } + + 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(); + auto& pointer = world->getUserPointer(); + + MWWorld::Ptr dropped; + if (pointer.canPlaceObject()) + dropped = world->placeObject(item.mBase, pointer.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& pointer = world->getUserPointer(); + if (world && pointer.getPointerTarget().mHit) + { + auto* node = pointer.getPointerTarget().mHitNode; + MWWorld::Ptr ptr = pointer.getPointerTarget().mHitObject; + auto wm = MWBase::Environment::get().getWindowManager(); + auto& dnd = wm->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()) + { + if (wm->isConsoleMode()) + wm->setConsoleSelectedObject(ptr); + // Don't active things during GUI mode. + else if (wm->isGuiMode()) + { + if (wm->getMode() != MWGui::GM_Container && wm->getMode() != MWGui::GM_Inventory) + return; + wm->getInventoryWindow()->pickUpObject(ptr); + } + else + { + MWWorld::Player& player = world->getPlayer(); + player.activate(ptr); + } + } + } + } + + void VRInputManager::injectMousePress(int sdlButton, bool onPress) + { + if (Environment::get().getGUIManager()->injectMouseClick(onPress)) + return; + + SDL_MouseButtonEvent arg; + if (onPress) + mMouseManager->mousePressed(arg, sdlButton); + else + mMouseManager->mouseReleased(arg, sdlButton); + } + + void VRInputManager::injectChannelValue( + MWInput::Actions action, + float value) + { + auto channel = mBindingsManager->ics().getChannel(MWInput::A_MoveLeftRight);// ->setValue(value); + channel->setEnabled(true); + } + + void VRInputManager::applyHapticsLeftHand(float intensity) + { + if (mHapticsEnabled) + mXRInput->getActionSet(ActionSet::Haptics).applyHaptics(TrackedLimb::LEFT_HAND, intensity); + } + + void VRInputManager::applyHapticsRightHand(float intensity) + { + if (mHapticsEnabled) + mXRInput->getActionSet(ActionSet::Haptics).applyHaptics(TrackedLimb::RIGHT_HAND, intensity); + } + + void VRInputManager::processChangedSettings(const std::set>& changed) + { + MWInput::InputManager::processChangedSettings(changed); + + for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) + { + if (it->first == "VR" && it->second == "haptics enabled") + { + mHapticsEnabled = Settings::Manager::getBool("haptics enabled", "VR"); + } + if (it->first == "Input" && it->second == "joystick dead zone") + { + setThumbstickDeadzone(Settings::Manager::getFloat("joystick dead zone", "Input")); + } + } + } + + void VRInputManager::throwDocumentError(TiXmlElement* element, std::string error) + { + std::stringstream ss; + ss << mXrControllerSuggestionsFile << "." << element->Row() << "." << element->Value(); + ss << ": " << error; + throw std::runtime_error(ss.str()); + } + + std::string VRInputManager::requireAttribute(TiXmlElement* element, std::string attribute) + { + const char* value = element->Attribute(attribute.c_str()); + if (!value) + throwDocumentError(element, std::string() + "Missing attribute '" + attribute + "'"); + return value; + } + + void VRInputManager::readInteractionProfile(TiXmlElement* element) + { + std::string interactionProfilePath = requireAttribute(element, "Path"); + mInteractionProfileLocalNames[interactionProfilePath] = requireAttribute(element, "LocalName"); + + Log(Debug::Verbose) << "Configuring interaction profile '" << interactionProfilePath << "' (" << mInteractionProfileLocalNames[interactionProfilePath] << ")"; + + // Check extension if present + TiXmlElement* extensionElement = element->FirstChildElement("Extension"); + if (extensionElement) + { + std::string extension = requireAttribute(extensionElement, "Name"); + auto xr = MWVR::Environment::get().getManager(); + if (!xr->xrExtensionIsEnabled(extension.c_str())) + { + Log(Debug::Verbose) << " Required extension '" << extension << "' not supported. Skipping interaction profile."; + return; + } + } + + TiXmlElement* actionSetGameplay = nullptr; + TiXmlElement* actionSetGUI = nullptr; + TiXmlElement* child = element->FirstChildElement("ActionSet"); + while (child) + { + std::string name = requireAttribute(child, "Name"); + if (name == "Gameplay") + actionSetGameplay = child; + else if (name == "GUI") + actionSetGUI = child; + + child = child->NextSiblingElement("ActionSet"); + } + + if (!actionSetGameplay) + throwDocumentError(element, "Gameplay action set missing"); + if (!actionSetGUI) + throwDocumentError(element, "GUI action set missing"); + + readInteractionProfileActionSet(actionSetGameplay, ActionSet::Gameplay, interactionProfilePath); + readInteractionProfileActionSet(actionSetGUI, ActionSet::GUI, interactionProfilePath); + mXRInput->suggestBindings(ActionSet::Tracking, interactionProfilePath, {}); + mXRInput->suggestBindings(ActionSet::Haptics, interactionProfilePath, {}); + } + + void VRInputManager::readInteractionProfileActionSet(TiXmlElement* element, ActionSet actionSet, std::string interactionProfilePath) + { + SuggestedBindings suggestedBindings; + + TiXmlElement* child = element->FirstChildElement("Binding"); + while (child) + { + std::string action = requireAttribute(child, "ActionName"); + std::string path = requireAttribute(child, "Path"); + + suggestedBindings.push_back( + SuggestedBinding{ + path, action + }); + + Log(Debug::Debug) << " " << action << ": " << path; + + child = child->NextSiblingElement("Binding"); + } + + mXRInput->suggestBindings(actionSet, interactionProfilePath, suggestedBindings); + } + + void VRInputManager::setThumbstickDeadzone(float deadzoneRadius) + { + mAxisDeadzone->setDeadzoneRadius(deadzoneRadius); + } + + void VRInputManager::requestRecenter(bool resetZ) + { + // TODO: Hack, should have a cleaner way of accessing this + reinterpret_cast(MWBase::Environment::get().getWorld()->getRenderingManager().getCamera())->requestRecenter(resetZ); + } + + 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, + const std::string& xrControllerSuggestionsFile) + : MWInput::InputManager( + window, + viewer, + screenCaptureHandler, + screenCaptureOperation, + userFile, + userFileExists, + userControllerBindingsFile, + controllerBindingsFile, + grab) + , mXRInput(new OpenXRInput(mAxisDeadzone)) + , mXrControllerSuggestionsFile(xrControllerSuggestionsFile) + , mHapticsEnabled{ Settings::Manager::getBool("haptics enabled", "VR") } + { + if (xrControllerSuggestionsFile.empty()) + throw std::runtime_error("No interaction profiles available (xrcontrollersuggestions.xml not found)"); + + Log(Debug::Verbose) << "Reading Input Profile Path suggestions from " << xrControllerSuggestionsFile; + + TiXmlDocument* xmlDoc = nullptr; + TiXmlElement* xmlRoot = nullptr; + + xmlDoc = new TiXmlDocument(xrControllerSuggestionsFile.c_str()); + xmlDoc->LoadFile(); + + if (xmlDoc->Error()) + { + std::ostringstream message; + message << "TinyXml reported an error reading \"" + xrControllerSuggestionsFile + "\". Row " << + (int)xmlDoc->ErrorRow() << ", Col " << (int)xmlDoc->ErrorCol() << ": " << + xmlDoc->ErrorDesc(); + Log(Debug::Error) << message.str(); + throw std::runtime_error(message.str()); + + delete xmlDoc; + return; + } + + xmlRoot = xmlDoc->RootElement(); + if (std::string(xmlRoot->Value()) != "Root") { + Log(Debug::Verbose) << "Error: Invalid xr controllers file. Missing element."; + delete xmlDoc; + return; + } + + TiXmlElement* profile = xmlRoot->FirstChildElement("Profile"); + while (profile) + { + readInteractionProfile(profile); + profile = profile->NextSiblingElement("Profile"); + } + + mXRInput->attachActionSets(); + setThumbstickDeadzone(Settings::Manager::getFloat("joystick dead zone", "Input")); + } + + 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(); + auto& actionSet = activeActionSet(); + actionSet.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 = actionSet.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) + return; + + // 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); + + if (!guiMode) + { + auto* world = MWBase::Environment::get().getWorld(); + + auto& player = world->getPlayer(); + auto playerPtr = world->getPlayerPtr(); + if (!mRealisticCombat || mRealisticCombat->ptr() != playerPtr) + { + auto trackingPath = Environment::get().getTrackingManager()->stringToVRPath("/user/hand/right/input/aim/pose"); + mRealisticCombat.reset(new RealisticCombat::StateMachine(playerPtr, trackingPath)); + } + 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* vrGuiManager = Environment::get().getGUIManager(); + auto* wm = MWBase::Environment::get().getWindowManager(); + + // 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. + if (wm->isPlayingVideo()) + { + 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)); + } + } + + bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); + + if (guiMode) + { + MyGUI::KeyCode key = MyGUI::KeyCode::None; + bool onPress = true; + + // Axis actions + switch (action->openMWActionCode()) + { + 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; + } + if (action->value() < 0.6f && action->previousValue() > 0.6f) + { + key = MyGUI::KeyCode::ArrowRight; + onPress = false; + } + if (action->value() > -0.6f && action->previousValue() < -0.6f) + { + key = MyGUI::KeyCode::ArrowLeft; + onPress = false; + } + 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; + } + if (action->value() < 0.6f && action->previousValue() > 0.6f) + { + key = MyGUI::KeyCode::ArrowUp; + onPress = false; + } + if (action->value() > -0.6f && action->previousValue() < -0.6f) + { + key = MyGUI::KeyCode::ArrowDown; + onPress = false; + } + 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 A_Recenter: + vrGuiManager->updateTracking(); + break; + case A_MenuSelect: + wm->injectKeyPress(MyGUI::KeyCode::Return, 0, false); + break; + case A_MenuBack: + if (MyGUI::InputManager::getInstance().isModalAny()) + wm->exitCurrentModal(); + else + wm->exitCurrentGuiMode(); + break; + case MWInput::A_Use: + 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); + pointActivation(false); + break; + case A_MenuSelect: + wm->injectKeyRelease(MyGUI::KeyCode::Return); + break; + default: + break; + } + } + + if (key != MyGUI::KeyCode::None) + { + if (onPress) + { + MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, 0); + } + else + { + MWBase::Environment::get().getWindowManager()->injectKeyRelease(key); + } + } + } + + else + { + if (disableControls) + { + return; + } + + // Hold actions + switch (action->openMWActionCode()) + { + case A_ActivateTouch: + resetIdleTime(); + mActivationIndication = action->isActive(); + break; + case MWInput::A_LookLeftRight: + { + float yaw = osg::DegreesToRadians(action->value()) * 200.f * dt; + // TODO: Hack, should have a cleaner way of accessing this + reinterpret_cast(MWBase::Environment::get().getWorld()->getRenderingManager().getCamera())->rotateStage(yaw); + 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 A_VrMetaMenu: + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_VrMetaMenu); + break; + case MWInput::A_Screenshot: + mActionManager->screenshot(); + break; + case MWInput::A_Inventory: + mActionManager->toggleInventory(); + 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: + vrGuiManager->updateTracking(); + if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) + requestRecenter(true); + 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; + default: + break; + } + } + } + } +} diff --git a/apps/openmw/mwvr/vrinputmanager.hpp b/apps/openmw/mwvr/vrinputmanager.hpp new file mode 100644 index 000000000..d30d94b9b --- /dev/null +++ b/apps/openmw/mwvr/vrinputmanager.hpp @@ -0,0 +1,95 @@ +#ifndef VR_INPUT_MANAGER_HPP +#define VR_INPUT_MANAGER_HPP + +#include "vrtypes.hpp" +#include "vrinput.hpp" + +#include "../mwinput/inputmanagerimp.hpp" + +#include +#include +#include + +#include "../mwworld/ptr.hpp" + +class TiXmlElement; + +namespace MWVR +{ + struct OpenXRInput; + struct OpenXRActionSet; + + namespace RealisticCombat { + class StateMachine; + } + + /// Extension of the input manager to include VR inputs + 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, + const std::string& xrControllerSuggestionsFile); + + virtual ~VRInputManager(); + + /// Overriden to force vr modes such as hiding cursors and crosshairs + void changeInputMode(bool guiMode) override; + + /// Overriden to update XR inputs + void update(float dt, bool disableControls = false, bool disableEvents = false) override; + + /// Set current offset to 0 and re-align VR stage. + void requestRecenter(bool resetZ); + + /// Tracking pose of the given limb at the given predicted time + Pose getLimbPose(int64_t time, TrackedLimb limb); + + /// Currently active action set + OpenXRActionSet& activeActionSet(); + + /// Notify input manager that the active interaction profile has changed + void notifyInteractionProfileChanged(); + + /// OpenXR input interface + OpenXRInput& xrInput() { return *mXRInput; } + + protected: + void processAction(const class Action* action, float dt, bool disableControls); + + void updateActivationIndication(void); + void pointActivation(bool onPress); + + void injectMousePress(int sdlButton, bool onPress); + void injectChannelValue(MWInput::Actions action, float value); + + void applyHapticsLeftHand(float intensity) override; + void applyHapticsRightHand(float intensity) override; + void processChangedSettings(const std::set< std::pair >& changed) override; + + void throwDocumentError(TiXmlElement* element, std::string error); + std::string requireAttribute(TiXmlElement* element, std::string attribute); + void readInteractionProfile(TiXmlElement* element); + void readInteractionProfileActionSet(TiXmlElement* element, ActionSet actionSet, std::string profilePath); + + void setThumbstickDeadzone(float deadzoneRadius); + + private: + std::shared_ptr mAxisDeadzone{ new AxisAction::Deadzone }; + std::unique_ptr mXRInput; + std::unique_ptr mRealisticCombat; + std::string mXrControllerSuggestionsFile; + bool mActivationIndication{ false }; + bool mHapticsEnabled{ true }; + + std::map mInteractionProfileLocalNames; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrlistbox.cpp b/apps/openmw/mwvr/vrlistbox.cpp new file mode 100644 index 000000000..6bd8a8941 --- /dev/null +++ b/apps/openmw/mwvr/vrlistbox.cpp @@ -0,0 +1,89 @@ +#include "vrlistbox.hpp" + +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/statemanager.hpp" + +namespace MWVR +{ + VrListBox::VrListBox() + : WindowModal("openmw_vr_listbox.layout") + , mOK(nullptr) + , mCancel(nullptr) + , mListBox(nullptr) + , mIndex(MyGUI::ITEM_NONE) + , mCallback() + { + getWidget(mOK, "OkButton"); + getWidget(mCancel, "CancelButton"); + getWidget(mListBox, "ListBox"); + + mOK->eventMouseButtonClick += MyGUI::newDelegate(this, &VrListBox::onOKButtonClicked); + mCancel->eventMouseButtonClick += MyGUI::newDelegate(this, &VrListBox::onCancelButtonClicked); + mListBox->eventListChangePosition += MyGUI::newDelegate(this, &VrListBox::onItemChanged); + mListBox->eventListSelectAccept += MyGUI::newDelegate(this, &VrListBox::onListAccept); + } + + VrListBox::~VrListBox() + { + + } + + void VrListBox::open(MyGUI::ComboBox* comboBox, Callback callback) + { + mCallback = callback; + mIndex = MyGUI::ITEM_NONE; + mListBox->removeAllItems(); + mListBox->setIndexSelected(MyGUI::ITEM_NONE); + + for (unsigned i = 0; i < comboBox->getItemCount(); i++) + mListBox->addItem(comboBox->getItemNameAt(i)); + + mListBox->setItemSelect(comboBox->getIndexSelected()); + + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mListBox); + + center(); + setVisible(true); + } + + void VrListBox::close() + { + setVisible(false); + } + + void VrListBox::onItemChanged(MyGUI::ListBox* _sender, size_t _index) + { + mIndex = _index; + } + + void VrListBox::onCancelButtonClicked(MyGUI::Widget* sender) + { + close(); + + if (mCallback) + mCallback(MyGUI::ITEM_NONE); + } + + void VrListBox::onOKButtonClicked(MyGUI::Widget* sender) + { + accept(mIndex); + } + + void VrListBox::onListAccept(MyGUI::ListBox* sender, size_t pos) + { + accept(pos); + } + void VrListBox::accept(size_t index) + { + close(); + + if (mCallback) + mCallback(index); + } +} diff --git a/apps/openmw/mwvr/vrlistbox.hpp b/apps/openmw/mwvr/vrlistbox.hpp new file mode 100644 index 000000000..b1839368f --- /dev/null +++ b/apps/openmw/mwvr/vrlistbox.hpp @@ -0,0 +1,48 @@ +#ifndef OPENMW_GAME_MWVR_LISTBOX_H +#define OPENMW_GAME_MWVR_LISTBOX_H + +#include "../mwgui/windowbase.hpp" + +#include +#include +#include "components/widgets/virtualkeyboardmanager.hpp" + +#include +#include +#include + +namespace Gui +{ + class VirtualKeyboardManager; +} + +namespace MWVR +{ + //! A simple dialogue that presents a list of choices. Used as an alternative to combo boxes in VR. + class VrListBox : public MWGui::WindowModal + { + public: + using Callback = std::function; + + VrListBox(); + ~VrListBox(); + + void open(MyGUI::ComboBox* comboBox, Callback callback); + void close(); + + private: + void onItemChanged(MyGUI::ListBox* _sender, size_t _index); + void onCancelButtonClicked(MyGUI::Widget* sender); + void onOKButtonClicked(MyGUI::Widget* sender); + void onListAccept(MyGUI::ListBox* sender, size_t pos); + void accept(size_t index); + + MyGUI::Widget* mCancel; + MyGUI::Widget* mOK; + MyGUI::ListBox* mListBox; + std::size_t mIndex; + Callback mCallback; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrmetamenu.cpp b/apps/openmw/mwvr/vrmetamenu.cpp new file mode 100644 index 000000000..b07cb78a5 --- /dev/null +++ b/apps/openmw/mwvr/vrmetamenu.cpp @@ -0,0 +1,155 @@ +#include "vrmetamenu.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/statemanager.hpp" + +#include "vrenvironment.hpp" +#include "vrinputmanager.hpp" + +namespace MWVR +{ + + VrMetaMenu::VrMetaMenu(int w, int h) + : WindowBase("openmw_vr_metamenu.layout") + , mWidth (w) + , mHeight (h) + { + updateMenu(); + } + + VrMetaMenu::~VrMetaMenu() + { + + } + + void VrMetaMenu::onResChange(int w, int h) + { + mWidth = w; + mHeight = h; + + updateMenu(); + } + + void VrMetaMenu::setVisible (bool visible) + { + if (visible) + updateMenu(); + + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["return"]); + + Layout::setVisible (visible); + } + + void VrMetaMenu::onFrame(float dt) + { + } + + void VrMetaMenu::onConsole() + { + if (MyGUI::InputManager::getInstance().isModalAny()) + return; + + MWBase::Environment::get().getWindowManager()->toggleConsole(); + } + + void VrMetaMenu::onGameMenu() + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); + } + + void VrMetaMenu::onJournal() + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Journal); + } + + void VrMetaMenu::onInventory() + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Inventory); + } + + void VrMetaMenu::onRest() + { + if (!MWBase::Environment::get().getWindowManager()->getRestEnabled() || MWBase::Environment::get().getWindowManager()->isGuiMode()) + return; + + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Rest); //Open rest GUI + } + + void VrMetaMenu::onQuickMenu() + { + MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_QuickKeysMenu); + } + + void VrMetaMenu::onQuickLoad() + { + if (!MyGUI::InputManager::getInstance().isModalAny()) + MWBase::Environment::get().getStateManager()->quickLoad(); + } + + void VrMetaMenu::onQuickSave() + { + if (!MyGUI::InputManager::getInstance().isModalAny()) + MWBase::Environment::get().getStateManager()->quickSave(); + } + + void VrMetaMenu::onRecenter() + { + Environment::get().getInputManager()->requestRecenter(true); + } + + void VrMetaMenu::close() + { + MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_VrMetaMenu); + } + + void VrMetaMenu::onButtonClicked(MyGUI::Widget *sender) + { + std::string name = *sender->getUserData(); + close(); + if (name == "console") + onConsole(); + else if (name == "gamemenu") + onGameMenu(); + else if (name == "journal") + onJournal(); + else if (name == "inventory") + onInventory(); + else if (name == "rest") + onRest(); + else if (name == "quickmenu") + onQuickMenu(); + else if (name == "quickload") + onQuickLoad(); + else if (name == "quicksave") + onQuickSave(); + else if (name == "recenter") + onRecenter(); + } + + bool VrMetaMenu::exit() + { + return MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Running; + } + + void VrMetaMenu::updateMenu() + { + static std::vector buttons{ "return", "recenter", "quicksave", "quickload", "console", "inventory", "journal", "rest", "quickmenu", "gamemenu" }; + + if(mButtons.empty()) + for (std::string& buttonId : buttons) + { + MyGUI::Button* button = nullptr; + getWidget(button, buttonId); + if (!button) + throw std::logic_error( std::string() + "Unable to find button \"" + buttonId + "\""); + button->eventMouseButtonClick += MyGUI::newDelegate(this, &VrMetaMenu::onButtonClicked); + button->setUserData(std::string(buttonId)); + button->setVisible(true); + mButtons[buttonId] = button; + } + } +} diff --git a/apps/openmw/mwvr/vrmetamenu.hpp b/apps/openmw/mwvr/vrmetamenu.hpp new file mode 100644 index 000000000..f1dffdab2 --- /dev/null +++ b/apps/openmw/mwvr/vrmetamenu.hpp @@ -0,0 +1,61 @@ +#ifndef OPENMW_GAME_MWVR_VRMETAMENU_H +#define OPENMW_GAME_MWVR_VRMETAMENU_H + +#include "../mwgui/windowbase.hpp" + +#include +#include "components/widgets/box.hpp" + +namespace Gui +{ + class ImageButton; +} + +namespace VFS +{ + class Manager; +} + +namespace MWVR +{ + class VrMetaMenu : public MWGui::WindowBase + { + int mWidth; + int mHeight; + + bool mHasAnimatedMenu; + + public: + + VrMetaMenu(int w, int h); + ~VrMetaMenu(); + + void onResChange(int w, int h) override; + + void setVisible (bool visible) override; + + void onFrame(float dt) override; + + bool exit() override; + + private: + std::map mButtons{}; + + void onButtonClicked (MyGUI::Widget* sender); + void onConsole(); + void onJournal(); + void onGameMenu(); + void onInventory(); + void onRest(); + void onQuickMenu(); + void onQuickLoad(); + void onQuickSave(); + void onRecenter(); + void close(); + + void updateMenu(); + }; + +} + +#endif diff --git a/apps/openmw/mwvr/vrpointer.cpp b/apps/openmw/mwvr/vrpointer.cpp new file mode 100644 index 000000000..7d5df7b70 --- /dev/null +++ b/apps/openmw/mwvr/vrpointer.cpp @@ -0,0 +1,203 @@ +#include "vrpointer.hpp" +#include "vrutil.hpp" +#include "vrenvironment.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwrender/renderingmanager.hpp" +#include "../mwrender/vismask.hpp" + +namespace MWVR +{ + UserPointer::UserPointer(osg::Group* root) + : mRoot(root) + { + mPointerGeometry = createPointerGeometry(); + mPointerRescale = new osg::MatrixTransform(); + mPointerRescale->addChild(mPointerGeometry); + mPointerTransform = new osg::MatrixTransform(); + mPointerTransform->addChild(mPointerRescale); + mPointerTransform->setName("Pointer Transform"); + mPointerTransform->setNodeMask(MWRender::VisMask::Mask_Pointer); + + auto tm = MWVR::Environment::get().getTrackingManager(); + tm->bind(this, "pcworld"); + mHandPath = tm->stringToVRPath("/user/hand/right/input/aim/pose"); + + setEnabled(true); + } + + UserPointer::~UserPointer() + { + } + + void UserPointer::setParent(osg::Group* group) + { + bool enabled = mEnabled; + setEnabled(false); + mParent = group; + setEnabled(enabled); + } + + void UserPointer::setEnabled(bool enabled) + { + mRoot->removeChild(mPointerTransform); + if (mParent) + { + mParent->removeChild(mPointerTransform); + if (enabled) + { + mParent->addChild(mPointerTransform); + // 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)))); + } + } + else if(enabled) + { + mRoot->addChild(mPointerTransform); + } + mEnabled = enabled; + } + + void UserPointer::onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) + { + // If no parent is set, then the actor is currently unloaded + // And we need to point directly from tracking data and the root + if (!mParent) + { + auto tp = source.getTrackingPose(predictedDisplayTime, mHandPath, 0); + osg::Matrix worldReference = osg::Matrix::identity(); + worldReference.preMultTranslate(tp.pose.position); + worldReference.preMultRotate(tp.pose.orientation); + mPointerTransform->setMatrix(worldReference); + updatePointerTarget(); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(mPointerGeometry); + } + } + + const MWRender::RayResult& UserPointer::getPointerTarget() const + { + return mPointerTarget; + } + + bool UserPointer::canPlaceObject() const + { + return mCanPlaceObject; + } + + void UserPointer::updatePointerTarget() + { + auto* world = MWBase::Environment::get().getWorld(); + if (world) + { + mPointerRescale->setMatrix(osg::Matrix::scale(1, 1, 1)); + + //mDistanceToPointerTarget = world->getTargetObject(mPointerTarget, mPointerTransform); + //osg::computeLocalToWorld(mPointerTransform->getParentalNodePaths()[0]); + mDistanceToPointerTarget = Util::getPoseTarget(mPointerTarget, Util::getNodePose(mPointerTransform), true); + + mCanPlaceObject = false; + if (mPointerTarget.mHit) + { + // check if the wanted position is on a flat surface, and not e.g. against a vertical wall + mCanPlaceObject = !(std::acos((mPointerTarget.mHitNormalWorld / mPointerTarget.mHitNormalWorld.length()) * osg::Vec3f(0, 0, 1)) >= osg::DegreesToRadians(30.f)); + } + + if (mDistanceToPointerTarget > 0.f) + mPointerRescale->setMatrix(osg::Matrix::scale(0.25f, mDistanceToPointerTarget, 0.25f)); + else + mPointerRescale->setMatrix(osg::Matrix::scale(0.25f, 10000.f, 0.25f)); + } + } + + osg::ref_ptr UserPointer::createPointerGeometry() + { + 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}, // A + {-1, 1, 1}, // B + {1, 1, 1}, // C + {1, 1, -1}, // D + }; + + 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 O = 0; + const int A = 1; + const int B = 2; + const int C = 3; + const int D = 4; + + const int triangles[] = + { + A,D,B, + B,D,C, + O,D,A, + O,C,D, + O,B,C, + O,A,B, + }; + 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]]; + } + + 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); + osg::ref_ptr fog(new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); + + osg::ref_ptr lightmodel = new osg::LightModel; + lightmodel->setAmbientIntensity(osg::Vec4(1.0, 1.0, 1.0, 1.0)); + stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); + SceneUtil::ShadowManager::disableShadowsForStateSet(stateset); + + osg::ref_ptr material = new osg::Material; + material->setColorMode(osg::Material::ColorMode::AMBIENT_AND_DIFFUSE); + stateset->setAttributeAndModes(material, osg::StateAttribute::ON); + + MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(geometry); + + return geometry; + } + +} diff --git a/apps/openmw/mwvr/vrpointer.hpp b/apps/openmw/mwvr/vrpointer.hpp new file mode 100644 index 000000000..bf285730d --- /dev/null +++ b/apps/openmw/mwvr/vrpointer.hpp @@ -0,0 +1,45 @@ +#ifndef MWVR_POINTER_H +#define MWVR_POINTER_H + +#include "../mwrender/npcanimation.hpp" +#include "../mwrender/renderingmanager.hpp" +#include "openxrmanager.hpp" +#include "vrsession.hpp" +#include "vrtracking.hpp" + +namespace MWVR +{ + //! Controls the beam used to target/select objects. + class UserPointer : public VRTrackingListener + { + public: + UserPointer(osg::Group* root); + ~UserPointer(); + + void updatePointerTarget(); + const MWRender::RayResult& getPointerTarget() const; + bool canPlaceObject() const; + void setParent(osg::Group* group); + void setEnabled(bool enabled); + protected: + void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) override; + + private: + osg::ref_ptr createPointerGeometry(); + + osg::ref_ptr mPointerGeometry{ nullptr }; + osg::ref_ptr mPointerRescale{ nullptr }; + osg::ref_ptr mPointerTransform{ nullptr }; + + osg::ref_ptr mParent{ nullptr }; + osg::ref_ptr mRoot{ nullptr }; + VRPath mHandPath; + + bool mEnabled; + MWRender::RayResult mPointerTarget{}; + float mDistanceToPointerTarget{ -1.f }; + bool mCanPlaceObject{ false }; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrsession.cpp b/apps/openmw/mwvr/vrsession.cpp new file mode 100644 index 000000000..bdc2c58fa --- /dev/null +++ b/apps/openmw/mwvr/vrsession.cpp @@ -0,0 +1,231 @@ +#include "vrsession.hpp" +#include "vrgui.hpp" +#include "vrenvironment.hpp" +#include "vrinputmanager.hpp" +#include "openxrmanager.hpp" +#include "openxrswapchain.hpp" +#include "../mwinput/inputmanagerimp.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/statemanager.hpp" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef max +#undef max +#endif + +#ifdef min +#undef min +#endif + +namespace MWVR +{ + VRSession::VRSession() + { + mSeatedPlay = Settings::Manager::getBool("seated play", "VR"); + } + + VRSession::~VRSession() + { + } + + void VRSession::processChangedSettings(const std::set>& changed) + { + setSeatedPlay(Settings::Manager::getBool("seated play", "VR")); + } + + void VRSession::beginFrame() + { + // Viewer traversals are sometimes entered without first updating the input manager. + if (getFrame(FramePhase::Update) == nullptr) + { + beginPhase(FramePhase::Update); + } + + } + + void VRSession::endFrame() + { + // Make sure we don't continue until the render thread has moved the current update frame to its next phase. + std::unique_lock lock(mMutex); + while (getFrame(FramePhase::Update)) + { + mCondition.wait(lock); + } + } + + void VRSession::setSeatedPlay(bool seatedPlay) + { + std::swap(mSeatedPlay, seatedPlay); + if (mSeatedPlay != seatedPlay) + { + Environment::get().getInputManager()->requestRecenter(true); + } + } + + static void swapConvention(osg::Vec3& v3) + { + + float y = v3.y(); + float z = v3.z(); + v3.y() = z; + v3.z() = -y; + } + + static void swapConvention(osg::Quat& q) + { + float y = q.y(); + float z = q.z(); + q.y() = z; + q.z() = -y; + } + + void VRSession::swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer) + { + auto* xr = Environment::get().getManager(); + + auto* frameMeta = getFrame(FramePhase::Draw).get(); + + if (frameMeta->mShouldSyncFrameLoop) + { + gc->swapBuffersImplementation(); + if (frameMeta->mShouldRender) + { + std::array layerStack{}; + layerStack[(int)Side::LEFT_SIDE].subImage = viewer.subImage(Side::LEFT_SIDE); + layerStack[(int)Side::RIGHT_SIDE].subImage = viewer.subImage(Side::RIGHT_SIDE); + layerStack[(int)Side::LEFT_SIDE].pose = frameMeta->mViews[(int)ReferenceSpace::STAGE][(int)Side::LEFT_SIDE].pose; + layerStack[(int)Side::RIGHT_SIDE].pose = frameMeta->mViews[(int)ReferenceSpace::STAGE][(int)Side::RIGHT_SIDE].pose; + layerStack[(int)Side::LEFT_SIDE].fov = frameMeta->mViews[(int)ReferenceSpace::STAGE][(int)Side::LEFT_SIDE].fov; + layerStack[(int)Side::RIGHT_SIDE].fov = frameMeta->mViews[(int)ReferenceSpace::STAGE][(int)Side::RIGHT_SIDE].fov; + xr->endFrame(frameMeta->mFrameInfo, &layerStack); + } + else + { + xr->endFrame(frameMeta->mFrameInfo, nullptr); + } + + xr->xrResourceReleased(); + } + + { + std::unique_lock lock(mMutex); + mLastRenderedFrame = frameMeta->mFrameNo; + getFrame(FramePhase::Draw) = nullptr; + } + + mCondition.notify_all(); + } + + void VRSession::beginPhase(FramePhase phase) + { + { + // TODO: Use a queue system + std::unique_lock lock(mMutex); + while (getFrame(phase)) + { + Log(Debug::Verbose) << "Warning: beginPhase called with a frame already in the target phase"; + mCondition.wait(lock); + } + } + + auto& frame = getFrame(phase); + + if (phase == FramePhase::Update) + { + prepareFrame(); + } + else + { + std::unique_lock lock(mMutex); + frame = std::move(getFrame(FramePhase::Update)); + } + + mCondition.notify_all(); + + if (phase == FramePhase::Draw && frame->mShouldSyncFrameLoop) + { + Environment::get().getManager()->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() + { + mFrames++; + + auto* xr = Environment::get().getManager(); + xr->handleEvents(); + auto& frame = getFrame(FramePhase::Update); + frame.reset(new VRFrameMeta); + + frame->mFrameNo = mFrames; + frame->mShouldSyncFrameLoop = xr->appShouldSyncFrameLoop(); + //frame->mShouldRender = xr->appShouldRender(); + if (frame->mShouldSyncFrameLoop) + { + frame->mFrameInfo = xr->waitFrame(); + frame->mShouldRender = frame->mFrameInfo.runtimeRequestsRender; + xr->xrResourceAcquired(); + } + } + + // 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); + } + + yaw = angle_z; + pitch = angle_x; + roll = angle_y; + } + +} + diff --git a/apps/openmw/mwvr/vrsession.hpp b/apps/openmw/mwvr/vrsession.hpp new file mode 100644 index 000000000..abe910fe7 --- /dev/null +++ b/apps/openmw/mwvr/vrsession.hpp @@ -0,0 +1,101 @@ +#ifndef MWVR_OPENRXSESSION_H +#define MWVR_OPENRXSESSION_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "openxrmanager.hpp" +#include "vrviewer.hpp" + +namespace MWVR +{ + + extern void getEulerAngles(const osg::Quat& quat, float& yaw, float& pitch, float& roll); + + /// \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 + { + 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 + Draw, //!< The frame currently in draw + NumPhases + }; + + struct VRFrameMeta + { + + DisplayTime mFrameNo{ 0 }; + std::array mViews[2]{}; + bool mShouldRender{ false }; + bool mShouldSyncFrameLoop{ false }; + FrameInfo mFrameInfo{}; + }; + + public: + VRSession(); + ~VRSession(); + + void swapBuffers(osg::GraphicsContext* gc, VRViewer& viewer); + + //! Starts a new frame + void prepareFrame(); + + void beginPhase(FramePhase phase); + std::unique_ptr& getFrame(FramePhase phase); + bool seatedPlay() const { return mSeatedPlay; } + + float playerScale() const { return mPlayerScale; } + void setPlayerScale(float scale) { mPlayerScale = scale; } + + float eyeLevel() const { return mEyeLevel; } + void setEyeLevel(float eyeLevel) { mEyeLevel = eyeLevel; } + + std::array, (int)FramePhase::NumPhases> mFrame{ nullptr }; + + void processChangedSettings(const std::set< std::pair >& changed); + + void beginFrame(); + void endFrame(); + + protected: + void setSeatedPlay(bool seatedPlay); + + private: + std::mutex mMutex{}; + std::condition_variable mCondition{}; + + bool mSeatedPlay{ false }; + 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 }; + float mEyeLevel{ 1.f }; + }; + +} + +#endif diff --git a/apps/openmw/mwvr/vrtracking.cpp b/apps/openmw/mwvr/vrtracking.cpp new file mode 100644 index 000000000..59f22ff9c --- /dev/null +++ b/apps/openmw/mwvr/vrtracking.cpp @@ -0,0 +1,342 @@ +#include "vrtracking.hpp" +#include "vrenvironment.hpp" +#include "vrsession.hpp" +#include "openxrmanagerimpl.hpp" + +#include + +#include + +namespace MWVR +{ + VRTrackingManager::VRTrackingManager() + { + mHandDirectedMovement = Settings::Manager::getBool("hand directed movement", "VR"); + mHeadPath = stringToVRPath("/user/head/input/pose"); + mHandPath = stringToVRPath("/user/hand/left/input/aim/pose"); + } + + VRTrackingManager::~VRTrackingManager() + { + } + + void VRTrackingManager::registerTrackingSource(VRTrackingSource* source, const std::string& name) + { + mSources.emplace(name, source); + notifySourceChanged(name); + } + + void VRTrackingManager::unregisterTrackingSource(VRTrackingSource* source) + { + std::string name = ""; + { + auto it = mSources.begin(); + while (it->second != source) it++; + name = it->first; + mSources.erase(it); + } + notifySourceChanged(name); + } + + VRTrackingSource* VRTrackingManager::getSource(const std::string& name) + { + auto it = mSources.find(name); + if (it != mSources.end()) + return it->second; + return nullptr; + } + + void VRTrackingManager::movementAngles(float& yaw, float& pitch) + { + yaw = mMovementYaw; + pitch = mMovementPitch; + } + + void VRTrackingManager::processChangedSettings(const std::set>& changed) + { + mHandDirectedMovement = Settings::Manager::getBool("hand directed movement", "VR"); + } + + void VRTrackingManager::notifySourceChanged(const std::string& name) + { + auto* source = getSource(name); + for (auto& it : mBindings) + { + if (it.second == name) + { + if (source) + it.first->onTrackingAttached(*source); + else + it.first->onTrackingDetached(); + } + } + } + + void VRTrackingManager::updateMovementAngles(DisplayTime predictedDisplayTime) + { + if (mHandDirectedMovement) + { + float headYaw = 0.f; + float headPitch = 0.f; + float headsWillRoll = 0.f; + + float handYaw = 0.f; + float handPitch = 0.f; + float handRoll = 0.f; + + auto pcsource = getSource("pcstage"); + + if (pcsource) + { + auto tpHead = pcsource->getTrackingPose(predictedDisplayTime, mHeadPath); + auto tpHand = pcsource->getTrackingPose(predictedDisplayTime, mHandPath); + + if (!!tpHead.status && !!tpHand.status) + { + getEulerAngles(tpHead.pose.orientation, headYaw, headPitch, headsWillRoll); + getEulerAngles(tpHand.pose.orientation, handYaw, handPitch, handRoll); + + mMovementYaw = handYaw - headYaw; + mMovementPitch = handPitch - headPitch; + } + } + } + else + { + mMovementYaw = 0; + mMovementPitch = 0; + } + } + + void VRTrackingManager::bind(VRTrackingListener* listener, std::string sourceName) + { + unbind(listener); + mBindings.emplace(listener, sourceName); + + auto* source = getSource(sourceName); + if (source) + listener->onTrackingAttached(*source); + else + listener->onTrackingDetached(); + } + + void VRTrackingManager::unbind(VRTrackingListener* listener) + { + auto it = mBindings.find(listener); + if (it != mBindings.end()) + { + listener->onTrackingDetached(); + mBindings.erase(listener); + } + } + + VRPath VRTrackingManager::stringToVRPath(const std::string& path) + { + // Empty path is invalid + if (path.empty()) + { + Log(Debug::Error) << "Empty path"; + return 0; + } + + // Return path immediately if it already exists + auto it = mPathIdentifiers.find(path); + if (it != mPathIdentifiers.end()) + return it->second; + + // Add new path and return it + auto res = mPathIdentifiers.emplace(path, mPathIdentifiers.size() + 1); + return res.first->second; + } + + std::string VRTrackingManager::VRPathToString(VRPath path) + { + // Find the identifier in the map and return the corresponding string. + for (auto& e : mPathIdentifiers) + if (e.second == path) + return e.first; + + // No path found, return empty string + Log(Debug::Warning) << "No such path identifier (" << path << ")"; + return ""; + } + + void VRTrackingManager::updateTracking() + { + MWVR::Environment::get().getSession()->endFrame(); + MWVR::Environment::get().getSession()->beginFrame(); + auto& frame = Environment::get().getSession()->getFrame(VRSession::FramePhase::Update); + + if (frame->mFrameInfo.runtimePredictedDisplayTime == 0) + return; + + for (auto source : mSources) + source.second->updateTracking(frame->mFrameInfo.runtimePredictedDisplayTime); + + updateMovementAngles(frame->mFrameInfo.runtimePredictedDisplayTime); + + for (auto& binding : mBindings) + { + auto* listener = binding.first; + auto* source = getSource(binding.second); + if (source) + { + if (source->availablePosesChanged()) + listener->onAvailablePosesChanged(*source); + listener->onTrackingUpdated(*source, frame->mFrameInfo.runtimePredictedDisplayTime); + } + } + + for (auto source : mSources) + source.second->clearAvailablePosesChanged(); + } + + VRTrackingSource::VRTrackingSource(const std::string& name) + { + Environment::get().getTrackingManager()->registerTrackingSource(this, name); + } + + VRTrackingSource::~VRTrackingSource() + { + Environment::get().getTrackingManager()->unregisterTrackingSource(this); + } + + VRTrackingPose VRTrackingSource::getTrackingPose(DisplayTime predictedDisplayTime, VRPath path, VRPath reference) + { + auto it = mCache.find(std::pair(path, reference)); + + if (it == mCache.end()) + { + mCache[std::pair(path, reference)] = getTrackingPoseImpl(predictedDisplayTime, path, reference); + mCache[std::pair(path, reference)].time = predictedDisplayTime; + } + + if (predictedDisplayTime <= it->second.time) + return it->second; + + auto tp = getTrackingPoseImpl(predictedDisplayTime, path, reference); + tp.time = predictedDisplayTime; + if (!tp.status) + tp.pose = it->second.pose; + it->second = tp; + + return tp; + } + + bool VRTrackingSource::availablePosesChanged() const + { + return mAvailablePosesChanged; + } + + void VRTrackingSource::clearAvailablePosesChanged() + { + mAvailablePosesChanged = false; + } + + void VRTrackingSource::clearCache() + { + mCache.clear(); + } + + void VRTrackingSource::notifyAvailablePosesChanged() + { + mAvailablePosesChanged = true; + } + + VRTrackingListener::~VRTrackingListener() + { + Environment::get().getTrackingManager()->unbind(this); + } + + VRTrackingToWorldBinding::VRTrackingToWorldBinding(const std::string& name, VRTrackingSource* source, VRPath movementReference) + : VRTrackingSource(name) + , mMovementReference(movementReference) + , mSource(source) + { + } + + + void VRTrackingToWorldBinding::setWorldOrientation(float yaw, bool adjust) + { + auto yawQuat = osg::Quat(yaw, osg::Vec3(0, 0, -1)); + if (adjust) + mOrientation = yawQuat * mOrientation; + else + mOrientation = yawQuat; + } + + osg::Vec3 VRTrackingToWorldBinding::movement() const + { + return mMovement; + } + + void VRTrackingToWorldBinding::consumeMovement(const osg::Vec3& movement) + { + mMovement.x() -= movement.x(); + mMovement.y() -= movement.y(); + } + + void VRTrackingToWorldBinding::recenter(bool resetZ) + { + mMovement.x() = 0; + mMovement.y() = 0; + if (resetZ) + { + if (mSeatedPlay) + mMovement.z() = mEyeLevel; + else + mMovement.z() = mLastPose.position.z(); + } + } + + VRTrackingPose VRTrackingToWorldBinding::getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath reference) + { + auto tp = mSource->getTrackingPose(predictedDisplayTime, path, reference); + tp.pose.position *= Constants::UnitsPerMeter; + + if (reference == 0 && !!tp.status) + { + tp.pose.position -= mLastPose.position; + tp.pose.position = mOrientation * tp.pose.position; + tp.pose.position += mMovement; + tp.pose.orientation = tp.pose.orientation * mOrientation; + + if(mOrigin) + tp.pose.position += mOriginWorldPose.position; + } + return tp; + } + + std::vector VRTrackingToWorldBinding::listSupportedTrackingPosePaths() const + { + return mSource->listSupportedTrackingPosePaths(); + } + + void VRTrackingToWorldBinding::updateTracking(DisplayTime predictedDisplayTime) + { + mOriginWorldPose = Pose(); + if (mOrigin) + { + auto worldMatrix = osg::computeLocalToWorld(mOrigin->getParentalNodePaths()[0]); + mOriginWorldPose.position = worldMatrix.getTrans(); + mOriginWorldPose.orientation = worldMatrix.getRotate(); + } + + auto mtp = mSource->getTrackingPose(predictedDisplayTime, mMovementReference, 0); + if (!!mtp.status) + { + mtp.pose.position *= Constants::UnitsPerMeter; + osg::Vec3 vrMovement = mtp.pose.position - mLastPose.position; + mLastPose = mtp.pose; + if (mHasTrackingData) + mMovement += mOrientation * vrMovement; + else + mMovement.z() = mLastPose.position.z(); + mHasTrackingData = true; + } + + mAvailablePosesChanged = mSource->availablePosesChanged(); + } +} + + diff --git a/apps/openmw/mwvr/vrtracking.hpp b/apps/openmw/mwvr/vrtracking.hpp new file mode 100644 index 000000000..31b804b38 --- /dev/null +++ b/apps/openmw/mwvr/vrtracking.hpp @@ -0,0 +1,223 @@ +#ifndef MWVR_VRTRACKING_H +#define MWVR_VRTRACKING_H + +#include +#include +#include +#include +#include +#include "vrtypes.hpp" + +namespace MWVR +{ + class VRAnimation; + + //! Describes the status of the tracking data. Note that there are multiple success statuses, and predicted poses should be used whenever the status is a non-negative integer. + enum class TrackingStatus : signed + { + Unknown = 0, //!< No data has been written (default value) + Good = 1, //!< Accurate, up-to-date tracking data was used. + Stale = 2, //!< Inaccurate, stale tracking data was used. This code is a status warning, not an error, and the tracking pose should be used. + NotTracked = -1, //!< No tracking data was returned because the tracking source does not track that + Lost = -2, //!< No tracking data was returned because the tracking source could not be read (occluded controller, network connectivity issues, etc.). + RuntimeFailure = -3 //!< No tracking data was returned because of a runtime failure. + }; + + inline bool operator!(TrackingStatus status) + { + return static_cast(status) < static_cast(TrackingStatus::Good); + } + + //! @brief An identifier representing an OpenXR path. 0 represents no path. + //! A VRPath can be optained from the string representation of a path using VRTrackingManager::getTrackingPath() + //! \note Support is determined by each VRTrackingSource. ALL strings are convertible to VRPaths but won't be useful unless they match a supported string. + //! \sa VRTrackingManager::getTrackingPath() + using VRPath = uint64_t; + + //! A single tracked pose + struct VRTrackingPose + { + TrackingStatus status = TrackingStatus::Unknown; //!< State of the prediction. + Pose pose = {}; //!< The predicted pose. + DisplayTime time; //!< The time for which the pose was predicted. + }; + + //! Source for tracking data. Converts paths to poses at predicted times. + //! \par The following paths are compulsory and must be supported by the implementation. + //! - /user/head/input/pose + //! - /user/hand/left/input/aim/pose + //! - /user/hand/right/input/aim/pose + //! - /user/hand/left/input/grip/pose (Not actually implemented yet) + //! - /user/hand/right/input/grip/pose (Not actually implemented yet) + //! \note A path being *supported* does not guarantee tracking data will be available at any given time (or ever). + //! \note Implementations may expand this list. + //! \sa OpenXRTracker VRGUITracking OpenXRTrackingToWorldBinding + class VRTrackingSource + { + public: + VRTrackingSource(const std::string& name); + virtual ~VRTrackingSource(); + + //! @brief Predicted pose of the given path at the predicted time + //! + //! \arg predictedDisplayTime[in] Time to predict. This is normally the predicted display time. If time is 0, the last pose that was predicted is returned. + //! \arg path[in] path of the pose requested. Should match an available pose path. + //! \arg reference[in] path of the pose to use as reference. If 0, pose is referenced to the VR stage. + //! + //! \return A structure describing a pose and the tracking status. + VRTrackingPose getTrackingPose(DisplayTime predictedDisplayTime, VRPath path, VRPath reference = 0); + + //! List currently supported tracking paths. + virtual std::vector listSupportedTrackingPosePaths() const = 0; + + //! Returns true if the available poses changed since the last frame, false otherwise. + bool availablePosesChanged() const; + + void clearAvailablePosesChanged(); + + //! Call once per frame, after (or at the end of) OSG update traversals and before cull traversals. + //! Predict tracked poses for the given display time. + //! \arg predictedDisplayTime [in] the predicted display time. The pose shall be predicted for this time based on current tracking data. + virtual void updateTracking(DisplayTime predictedDisplayTime) = 0; + + void clearCache(); + + protected: + virtual VRTrackingPose getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath reference = 0) = 0; + + std::map, VRTrackingPose> mCache; + + void notifyAvailablePosesChanged(); + + bool mAvailablePosesChanged = true; + }; + + //! Ties a tracking source to the game world. + //! A reference pose is selected by passing its path to the constructor. + //! All poses are transformed in the horizontal plane by moving the x,y origin to the position of the reference pose, and then reoriented using the current orientation of the player character. + //! The reference pose is effectively always at the x,y origin, and its movement is accumulated and can be read using the movement() call. + //! If this movement is ever consumed (such as by moving the character to follow the player) the consumed movement must be reported using consumeMovement(). + class VRTrackingToWorldBinding : public VRTrackingSource + { + public: + VRTrackingToWorldBinding(const std::string& name, VRTrackingSource* source, VRPath reference); + + void setWorldOrientation(float yaw, bool adjust); + osg::Quat getWorldOrientation() const { return mOrientation; } + + void setEyeLevel(float eyeLevel) { mEyeLevel = eyeLevel; } + float getEyeLevel() const { return mEyeLevel; } + + void setSeatedPlay(bool seatedPlay) { mSeatedPlay = seatedPlay; } + bool getSeatedPlay() const { return mSeatedPlay; } + + //! The player's movement within the VR stage. This accumulates until the movement has been consumed by calling consumeMovement() + osg::Vec3 movement() const; + + //! Consume movement + void consumeMovement(const osg::Vec3& movement); + + //! Recenter tracking by consuming all movement. + void recenter(bool resetZ); + + //! World origin is the point that ties the stage and the world. (0,0,0 in the world-aligned stage is this node). + //! If no node is set, the world-aligned stage and the world correspond 1-1. + void setOriginNode(osg::Node* origin) { mOrigin = origin; } + + protected: + //! Fetches a pose from the source, and then aligns it with the game world if the reference is 0 (stage). + VRTrackingPose getTrackingPoseImpl(DisplayTime predictedDisplayTime, VRPath path, VRPath movementReference = 0) override; + + //! List currently supported tracking paths. + std::vector listSupportedTrackingPosePaths() const override; + + //! Call once per frame, after (or at the end of) OSG update traversals and before cull traversals. + //! Predict tracked poses for the given display time. + //! \arg predictedDisplayTime [in] the predicted display time. The pose shall be predicted for this time based on current tracking data. + void updateTracking(DisplayTime predictedDisplayTime) override; + + private: + VRPath mMovementReference; + VRTrackingSource* mSource; + osg::Node* mOrigin = nullptr; + bool mSeatedPlay = false; + bool mHasTrackingData = false; + float mEyeLevel = 0; + Pose mOriginWorldPose = Pose(); + Pose mLastPose = Pose(); + osg::Vec3 mMovement = osg::Vec3(0, 0, 0); + osg::Quat mOrientation = osg::Quat(0, 0, 0, 1); + }; + + class VRTrackingListener + { + public: + virtual ~VRTrackingListener(); + + //! Notify that available tracking poses have changed. + virtual void onAvailablePosesChanged(VRTrackingSource& source) {}; + + //! Notify that a tracking source has been attached + virtual void onTrackingAttached(VRTrackingSource& source) {}; + + //! Notify that a tracking source has been detached. + virtual void onTrackingDetached() {}; + + //! Called every frame after tracking poses have been updated + virtual void onTrackingUpdated(VRTrackingSource& source, DisplayTime predictedDisplayTime) {}; + + private: + }; + + class VRTrackingManager + { + public: + VRTrackingManager(); + ~VRTrackingManager(); + + //! Angles to be used for overriding movement direction + //void movementAngles(float& yaw, float& pitch); + + void updateTracking(); + + //! Bind listener to source, listener will receive tracking updates from source until unbound. + //! \note A single listener can only receive tracking updates from one source. + void bind(VRTrackingListener* listener, std::string source); + + //! Unbind listener, listener will no longer receive tracking updates. + void unbind(VRTrackingListener* listener); + + //! Converts a string representation of a path to a VRTrackerPath identifier + VRPath stringToVRPath(const std::string& path); + + //! Converts a path identifier back to string. Returns an empty string if no such identifier exists. + std::string VRPathToString(VRPath path); + + //! Get a tracking source by name + VRTrackingSource* getSource(const std::string& name); + + //! Angles to be used for overriding movement direction + void movementAngles(float& yaw, float& pitch); + + void processChangedSettings(const std::set< std::pair >& changed); + + private: + friend class VRTrackingSource; + void registerTrackingSource(VRTrackingSource* source, const std::string& name); + void unregisterTrackingSource(VRTrackingSource* source); + void notifySourceChanged(const std::string& name); + void updateMovementAngles(DisplayTime predictedDisplayTime); + + std::map mSources; + std::map mBindings; + std::map mPathIdentifiers; + + bool mHandDirectedMovement = 0.f; + VRPath mHeadPath = 0; + VRPath mHandPath = 0; + float mMovementYaw = 0.f; + float mMovementPitch = 0.f; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrtypes.cpp b/apps/openmw/mwvr/vrtypes.cpp new file mode 100644 index 000000000..4ee6cbe74 --- /dev/null +++ b/apps/openmw/mwvr/vrtypes.cpp @@ -0,0 +1,204 @@ +#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) + { + 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; + } + +} + + diff --git a/apps/openmw/mwvr/vrtypes.hpp b/apps/openmw/mwvr/vrtypes.hpp new file mode 100644 index 000000000..ad5984ba4 --- /dev/null +++ b/apps/openmw/mwvr/vrtypes.hpp @@ -0,0 +1,149 @@ +#ifndef MWVR_VRTYPES_H +#define MWVR_VRTYPES_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MWVR +{ + class OpenXRSwapchain; + class OpenXRManager; + + //! 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. + }; + + //! Self-descriptive + enum class Side + { + LEFT_SIDE = 0, + RIGHT_SIDE = 1 + }; + + using DisplayTime = int64_t; + + ////! 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); + + // //! 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; + //}; + + ////! Fov of a single eye + //struct FieldOfView { + // float angleLeft; + // float angleRight; + // float angleUp; + // float angleDown; + + // bool operator==(const FieldOfView& rhs) const; + + // //! 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; + //}; + + using Pose = Misc::Pose; + using FieldOfView = Misc::FieldOfView; + using View = Misc::View; + + //! 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; + }; + + struct SubImage + { + class OpenXRSwapchain* swapchain; + int32_t x; + int32_t y; + int32_t width; + int32_t height; + }; + + struct CompositionLayerProjectionView + { + SubImage subImage; + Pose pose; + FieldOfView fov; + }; + + struct SwapchainConfig + { + int recommendedWidth = -1; + int recommendedHeight = -1; + int recommendedSamples = -1; + int maxWidth = -1; + int maxHeight = -1; + int maxSamples = -1; + int selectedWidth = -1; + int selectedHeight = -1; + int selectedSamples = -1; + int offsetWidth = 0; + int offsetHeight = 0; + std::string name = ""; + }; + + struct FrameInfo + { + long long runtimePredictedDisplayTime; + long long runtimePredictedDisplayPeriod; + bool runtimeRequestsRender; + }; + + // 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); +} + +#endif diff --git a/apps/openmw/mwvr/vrutil.cpp b/apps/openmw/mwvr/vrutil.cpp new file mode 100644 index 000000000..311c27fd6 --- /dev/null +++ b/apps/openmw/mwvr/vrutil.cpp @@ -0,0 +1,80 @@ +#include "vrutil.hpp" +#include "vrenvironment.hpp" +#include "vrtracking.hpp" +#include "vranimation.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwworld/class.hpp" +#include "../mwrender/renderingmanager.hpp" + +#include "osg/Transform" + +namespace MWVR +{ + namespace Util + { + std::pair getHitContact(float distance, std::vector& targets) + { + return std::pair(); + } + + std::pair getTouchTarget() + { + MWRender::RayResult result; + auto* tm = Environment::get().getTrackingManager(); + VRPath rightHandPath = tm->stringToVRPath("/user/hand/right/input/aim/pose"); + auto* source = tm->getSource("pcworld"); + auto distance = getPoseTarget(result, source->getTrackingPose(0, rightHandPath).pose, true); + return std::pair(result.mHitObject, distance); + } + + std::pair getWeaponTarget() + { + auto* anim = MWVR::Environment::get().getPlayerAnimation(); + + MWRender::RayResult result; + auto distance = getPoseTarget(result, getNodePose(anim->getNode("weapon bone")), false); + return std::pair(result.mHitObject, distance); + } + + float getPoseTarget(MWRender::RayResult& result, const Pose& pose, bool allowTelekinesis) + { + auto* wm = MWBase::Environment::get().getWindowManager(); + auto* world = MWBase::Environment::get().getWorld(); + + if (wm->isGuiMode() && wm->isConsoleMode()) + return world->getTargetObject(result, pose.position, pose.orientation, world->getMaxActivationDistance() * 50, true); + else + { + float activationDistance = 0.f; + if (allowTelekinesis) + activationDistance = world->getActivationDistancePlusTelekinesis(); + else + activationDistance = world->getMaxActivationDistance(); + + auto distance = world->getTargetObject(result, pose.position, pose.orientation, world->getMaxActivationDistance(), true); + + if (!result.mHitObject.isEmpty() && !result.mHitObject.getClass().allowTelekinesis(result.mHitObject) + && distance > world->getMaxActivationDistance() && !MWBase::Environment::get().getWindowManager()->isGuiMode()) + { + result.mHit = false; + result.mHitObject = nullptr; + distance = 0.f; + }; + return distance; + } + } + + Pose getNodePose(const osg::Node* node) + { + osg::Matrix worldMatrix = osg::computeLocalToWorld(node->getParentalNodePaths()[0]); + Pose pose; + pose.position = worldMatrix.getTrans(); + pose.orientation = worldMatrix.getRotate(); + return pose; + } + } +} diff --git a/apps/openmw/mwvr/vrutil.hpp b/apps/openmw/mwvr/vrutil.hpp new file mode 100644 index 000000000..ed4a12cd0 --- /dev/null +++ b/apps/openmw/mwvr/vrutil.hpp @@ -0,0 +1,24 @@ +#ifndef VR_UTIL_HPP +#define VR_UTIL_HPP + +#include "../mwworld/ptr.hpp" + +#include "vrtypes.hpp" + +namespace MWRender +{ + struct RayResult; +} + +namespace MWVR +{ + namespace Util { + std::pair getTouchTarget(); + std::pair getWeaponTarget(); + float getPoseTarget(MWRender::RayResult& result, const Pose& pose, bool allowTelekinesis); + Pose getNodePose(const osg::Node* node); + } + +} + +#endif diff --git a/apps/openmw/mwvr/vrviewer.cpp b/apps/openmw/mwvr/vrviewer.cpp new file mode 100644 index 000000000..807256de1 --- /dev/null +++ b/apps/openmw/mwvr/vrviewer.cpp @@ -0,0 +1,529 @@ +#include "vrviewer.hpp" + +#include "openxrmanagerimpl.hpp" +#include "openxrswapchain.hpp" +#include "vrenvironment.hpp" +#include "vrsession.hpp" +#include "vrframebuffer.hpp" + +#include "../mwrender/vismask.hpp" + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include + +#include + +namespace MWVR +{ + // Callback to do construction with a graphics context + class RealizeOperation : public osg::GraphicsOperation + { + public: + RealizeOperation() : osg::GraphicsOperation("VRRealizeOperation", false) {}; + void operator()(osg::GraphicsContext* gc) override; + bool realized(); + + private: + }; + + VRViewer::VRViewer( + osg::ref_ptr viewer) + : mViewer(viewer) + , mPreDraw(new PredrawCallback(this)) + , mPostDraw(new PostdrawCallback(this)) + , mFinalDraw(new FinaldrawCallback(this)) + , mUpdateViewCallback(new UpdateViewCallback(this)) + , mMsaaResolveTexture{} + , mMirrorTexture{ nullptr } + , mOpenXRConfigured(false) + , mCallbacksConfigured(false) + { + mViewer->setRealizeOperation(new RealizeOperation()); + } + + VRViewer::~VRViewer(void) + { + } + + int parseResolution(std::string conf, int recommended, int max) + { + if (Misc::StringUtils::isNumber(conf)) + { + int res = std::atoi(conf.c_str()); + if (res <= 0) + return recommended; + if (res > max) + return max; + return res; + } + conf = Misc::StringUtils::lowerCase(conf); + if (conf == "auto" || conf == "recommended") + { + return recommended; + } + if (conf == "max") + { + return max; + } + return recommended; + } + + static VRViewer::MirrorTextureEye mirrorTextureEyeFromString(const std::string& str) + { + if (Misc::StringUtils::ciEqual(str, "left")) + return VRViewer::MirrorTextureEye::Left; + if (Misc::StringUtils::ciEqual(str, "right")) + return VRViewer::MirrorTextureEye::Right; + if (Misc::StringUtils::ciEqual(str, "both")) + return VRViewer::MirrorTextureEye::Both; + return VRViewer::MirrorTextureEye::Both; + } + + void VRViewer::configureXR(osg::GraphicsContext* gc) + { + std::unique_lock lock(mMutex); + + if (mOpenXRConfigured) + { + return; + } + + + auto* xr = Environment::get().getManager(); + xr->realize(gc); + + // Set up swapchain config + mSwapchainConfig = xr->getRecommendedSwapchainConfig(); + + std::array xConfString; + std::array yConfString; + xConfString[0] = Settings::Manager::getString("left eye resolution x", "VR"); + yConfString[0] = Settings::Manager::getString("left eye resolution y", "VR"); + + xConfString[1] = Settings::Manager::getString("right eye resolution x", "VR"); + yConfString[1] = Settings::Manager::getString("right eye resolution y", "VR"); + + std::array viewNames = { + "LeftEye", + "RightEye" + }; + for (unsigned i = 0; i < viewNames.size(); i++) + { + auto name = viewNames[i]; + + mSwapchainConfig[i].selectedWidth = parseResolution(xConfString[i], mSwapchainConfig[i].recommendedWidth, mSwapchainConfig[i].maxWidth); + mSwapchainConfig[i].selectedHeight = parseResolution(yConfString[i], mSwapchainConfig[i].recommendedHeight, mSwapchainConfig[i].maxHeight); + + mSwapchainConfig[i].selectedSamples = + std::max(1, // OpenXR requires a non-zero value + std::min(mSwapchainConfig[i].maxSamples, + Settings::Manager::getInt("antialiasing", "Video") + ) + ); + + Log(Debug::Verbose) << name << " resolution: Recommended x=" << mSwapchainConfig[i].recommendedWidth << ", y=" << mSwapchainConfig[i].recommendedHeight; + Log(Debug::Verbose) << name << " resolution: Max x=" << mSwapchainConfig[i].maxWidth << ", y=" << mSwapchainConfig[i].maxHeight; + Log(Debug::Verbose) << name << " resolution: Selected x=" << mSwapchainConfig[i].selectedWidth << ", y=" << mSwapchainConfig[i].selectedHeight; + + mSwapchainConfig[i].name = name; + if (i > 0) + mSwapchainConfig[i].offsetWidth = mSwapchainConfig[i].selectedWidth + mSwapchainConfig[i].offsetWidth; + + mSwapchain[i].reset(new OpenXRSwapchain(gc->getState(), mSwapchainConfig[i])); + mSubImages[i].width = mSwapchainConfig[i].selectedWidth; + mSubImages[i].height = mSwapchainConfig[i].selectedHeight; + mSubImages[i].x = mSubImages[i].y = 0; + mSubImages[i].swapchain = mSwapchain[i].get(); + } + + int width = mSubImages[0].width + mSubImages[1].width; + int height = std::max(mSubImages[0].height, mSubImages[1].height); + int samples = std::max(mSwapchainConfig[0].selectedSamples, mSwapchainConfig[1].selectedSamples); + + mFramebuffer.reset(new VRFramebuffer(gc->getState(), width, height, samples)); + mFramebuffer->createColorBuffer(gc); + mFramebuffer->createDepthBuffer(gc); + mMsaaResolveTexture.reset(new VRFramebuffer(gc->getState(), width, height, 0)); + mMsaaResolveTexture->createColorBuffer(gc); + mGammaResolveTexture.reset(new VRFramebuffer(gc->getState(), width, height, 0)); + mGammaResolveTexture->createColorBuffer(gc); + mGammaResolveTexture->createDepthBuffer(gc); + + mViewer->setReleaseContextAtEndOfFrameHint(false); + mViewer->getCamera()->getGraphicsContext()->setSwapCallback(new VRViewer::SwapBuffersCallback(this)); + + setupMirrorTexture(); + Log(Debug::Verbose) << "XR configured"; + mOpenXRConfigured = true; + } + + void VRViewer::configureCallbacks() + { + if (mCallbacksConfigured) + return; + + // Give the main camera an initial draw callback that disables camera setup (we don't want it) + Misc::StereoView::instance().setUpdateViewCallback(mUpdateViewCallback); + Misc::CallbackManager::instance().addCallback(Misc::CallbackManager::DrawStage::Initial, new InitialDrawCallback(this)); + Misc::CallbackManager::instance().addCallback(Misc::CallbackManager::DrawStage::PreDraw, mPreDraw); + Misc::CallbackManager::instance().addCallback(Misc::CallbackManager::DrawStage::PostDraw, mPostDraw); + Misc::CallbackManager::instance().addCallback(Misc::CallbackManager::DrawStage::Final, mFinalDraw); + auto cullMask = ~(MWRender::VisMask::Mask_UpdateVisitor | MWRender::VisMask::Mask_SimpleWater); + cullMask &= ~MWRender::VisMask::Mask_GUI; + cullMask |= MWRender::VisMask::Mask_3DGUI; + Misc::StereoView::instance().setCullMask(cullMask); + + mCallbacksConfigured = true; + } + + void VRViewer::setupMirrorTexture() + { + mMirrorTextureEnabled = Settings::Manager::getBool("mirror texture", "VR"); + mMirrorTextureEye = mirrorTextureEyeFromString(Settings::Manager::getString("mirror texture eye", "VR")); + mFlipMirrorTextureOrder = Settings::Manager::getBool("flip mirror texture order", "VR"); + mMirrorTextureShouldBeCleanedUp = true; + + mMirrorTextureViews.clear(); + if (mMirrorTextureEye == MirrorTextureEye::Left || mMirrorTextureEye == MirrorTextureEye::Both) + mMirrorTextureViews.push_back(Side::LEFT_SIDE); + if (mMirrorTextureEye == MirrorTextureEye::Right || mMirrorTextureEye == MirrorTextureEye::Both) + mMirrorTextureViews.push_back(Side::RIGHT_SIDE); + if (mFlipMirrorTextureOrder) + std::reverse(mMirrorTextureViews.begin(), mMirrorTextureViews.end()); + // TODO: If mirror is false either hide the window or paste something meaningful into it. + // E.g. Fanart of Dagoth UR wearing a VR headset + } + + void VRViewer::processChangedSettings(const std::set>& changed) + { + bool mirrorTextureChanged = false; + for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) + { + if (it->first == "VR" && it->second == "mirror texture") + { + mirrorTextureChanged = true; + } + if (it->first == "VR" && it->second == "mirror texture eye") + { + mirrorTextureChanged = true; + } + if (it->first == "VR" && it->second == "flip mirror texture order") + { + mirrorTextureChanged = true; + } + } + + if (mirrorTextureChanged) + setupMirrorTexture(); + } + + SubImage VRViewer::subImage(Side side) + { + return mSubImages[static_cast(side)]; + } + + static GLuint createShader(osg::GLExtensions* gl, const char* source, GLenum type) + { + GLint len = strlen(source); + GLuint shader = gl->glCreateShader(type); + gl->glShaderSource(shader, 1, &source, &len); + gl->glCompileShader(shader); + GLint isCompiled = 0; + gl->glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled); + if (isCompiled == GL_FALSE) + { + GLint maxLength = 0; + gl->glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); + std::vector infoLog(maxLength); + gl->glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]); + gl->glDeleteShader(shader); + + Log(Debug::Error) << "Failed to compile shader: " << infoLog.data(); + + return 0; + } + return shader; + } + + static bool applyGamma(osg::RenderInfo& info, VRFramebuffer& target, VRFramebuffer& source) + { + osg::State* state = info.getState(); + static const char* vSource = "#version 120\n varying vec2 uv; void main(){ gl_Position = vec4(gl_Vertex.xy*2.0 - 1, 0, 1); uv = gl_Vertex.xy;}"; + static const char* fSource = "#version 120\n varying vec2 uv; uniform sampler2D t; uniform float gamma; uniform float contrast;" + "void main() {" + "vec4 color1 = texture2D(t, uv);" + "vec3 rgb = color1.rgb;" + "rgb = (rgb - 0.5f) * contrast + 0.5f;" + "rgb = pow(rgb, vec3(1.0/gamma));" + "gl_FragColor = vec4(rgb, color1.a);" + "}"; + + static bool first = true; + static osg::ref_ptr program = nullptr; + static osg::ref_ptr vShader = nullptr; + static osg::ref_ptr fShader = nullptr; + static osg::ref_ptr gammaUniform = nullptr; + static osg::ref_ptr contrastUniform = nullptr; + osg::Viewport* viewport = nullptr; + static osg::ref_ptr stateset = nullptr; + static osg::ref_ptr geometry = nullptr; + static osg::ref_ptr texture = nullptr; + static osg::ref_ptr textureObject = nullptr; + + static std::vector vertices = + { + {0, 0, 0, 0}, + {1, 0, 0, 0}, + {1, 1, 0, 0}, + {0, 0, 0, 0}, + {1, 1, 0, 0}, + {0, 1, 0, 0} + }; + static osg::ref_ptr vertexArray = new osg::Vec4Array(vertices.begin(), vertices.end()); + + if (first) + { + geometry = new osg::Geometry(); + geometry->setVertexArray(vertexArray); + geometry->addPrimitiveSet(new osg::DrawArrays(GL_TRIANGLES, 0, 6)); + geometry->setUseDisplayList(false); + stateset = geometry->getOrCreateStateSet(); + + vShader = new osg::Shader(osg::Shader::Type::VERTEX, vSource); + fShader = new osg::Shader(osg::Shader::Type::FRAGMENT, fSource); + program = new osg::Program(); + program->addShader(vShader); + program->addShader(fShader); + program->compileGLObjects(*state); + stateset->setAttributeAndModes(program, osg::StateAttribute::ON); + + texture = new osg::Texture2D(); + texture->setName("diffuseMap"); + textureObject = new osg::Texture::TextureObject(texture, source.colorBuffer(), GL_TEXTURE_2D); + texture->setTextureObject(state->getContextID(), textureObject); + stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::PROTECTED); + stateset->setTextureMode(0, GL_TEXTURE_2D, osg::StateAttribute::PROTECTED); + + gammaUniform = new osg::Uniform("gamma", Settings::Manager::getFloat("gamma", "Video")); + contrastUniform = new osg::Uniform("contrast", Settings::Manager::getFloat("contrast", "Video")); + stateset->addUniform(gammaUniform); + stateset->addUniform(contrastUniform); + + geometry->compileGLObjects(info); + + first = false; + } + + target.bindFramebuffer(state->getGraphicsContext(), GL_FRAMEBUFFER_EXT); + + if (program != nullptr) + { + // OSG does not pop statesets until after the final draw callback. Unrelated statesets may therefore still be on the stack at this point. + // Pop these to avoid inheriting arbitrary state from these. They will not be used more in this frame. + state->popAllStateSets(); + state->apply(); + + gammaUniform->set(Settings::Manager::getFloat("gamma", "Video")); + contrastUniform->set(Settings::Manager::getFloat("contrast", "Video")); + state->pushStateSet(stateset); + state->apply(); + + if(!viewport) + viewport = new osg::Viewport(0, 0, target.width(), target.height()); + viewport->setViewport(0, 0, target.width(), target.height()); + viewport->apply(*state); + + glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + geometry->draw(info); + + state->popStateSet(); + return true; + } + return false; + } + + void VRViewer::blit(osg::RenderInfo& info) + { + if (mMirrorTextureShouldBeCleanedUp) + { + mMirrorTexture = nullptr; + mMirrorTextureShouldBeCleanedUp = false; + } + + auto* state = info.getState(); + auto* gc = state->getGraphicsContext(); + auto* gl = osg::GLExtensions::Get(state->getContextID(), false); + + auto* traits = SDLUtil::GraphicsWindowSDL2::findContext(*mViewer)->getTraits(); + int screenWidth = traits->width; + int screenHeight = traits->height; + + mMsaaResolveTexture->bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + + mFramebuffer->blit(gc, 0, 0, mFramebuffer->width(), mFramebuffer->height(), 0, 0, mMsaaResolveTexture->width(), mMsaaResolveTexture->height(), GL_COLOR_BUFFER_BIT, GL_NEAREST); + + bool shouldDoGamma = Settings::Manager::getBool("gamma postprocessing", "VR Debug"); + if (!shouldDoGamma || !applyGamma(info, *mGammaResolveTexture, *mMsaaResolveTexture)) + { + mGammaResolveTexture->bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + mMsaaResolveTexture->blit(gc, 0, 0, mMsaaResolveTexture->width(), mMsaaResolveTexture->height(), 0, 0, mGammaResolveTexture->width(), mGammaResolveTexture->height(), GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + + mGammaResolveTexture->bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + mFramebuffer->blit(gc, 0, 0, mFramebuffer->width(), mFramebuffer->height(), 0, 0, mGammaResolveTexture->width(), mGammaResolveTexture->height(), GL_DEPTH_BUFFER_BIT, GL_NEAREST); + + //// Since OpenXR does not include native support for mirror textures, we have to generate them ourselves + if (mMirrorTextureEnabled) + { + if (!mMirrorTexture) + { + mMirrorTexture.reset(new VRFramebuffer(state, + screenWidth, + screenHeight, + 0)); + mMirrorTexture->createColorBuffer(gc); + } + + int dstWidth = screenWidth / mMirrorTextureViews.size(); + int srcWidth = mGammaResolveTexture->width() / 2; + int dstX = 0; + mMirrorTexture->bindFramebuffer(gc, GL_FRAMEBUFFER_EXT); + for (auto viewId : mMirrorTextureViews) + { + int srcX = static_cast(viewId) * srcWidth; + mGammaResolveTexture->blit(gc, srcX, 0, srcX + srcWidth, mGammaResolveTexture->height(), dstX, 0, dstX + dstWidth, screenHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR); + dstX += dstWidth; + } + + gl->glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0); + mMirrorTexture->blit(gc, 0, 0, screenWidth, screenHeight, 0, 0, screenWidth, screenHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + + mSwapchain[0]->endFrame(gc, *mGammaResolveTexture); + mSwapchain[1]->endFrame(gc, *mGammaResolveTexture); + gl->glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0); + } + + void + VRViewer::SwapBuffersCallback::swapBuffersImplementation( + osg::GraphicsContext* gc) + { + mViewer->swapBuffersCallback(gc); + } + + void + RealizeOperation::operator()( + osg::GraphicsContext* gc) + { + if (Debug::shouldDebugOpenGL()) + Debug::EnableGLDebugOperation()(gc); + + Environment::get().getViewer()->configureXR(gc); + } + + bool + RealizeOperation::realized() + { + return Environment::get().getViewer()->xrConfigured(); + } + + void VRViewer::initialDrawCallback(osg::RenderInfo& info) + { + if (mRenderingReady) + return; + + Environment::get().getSession()->beginPhase(VRSession::FramePhase::Draw); + if (Environment::get().getSession()->getFrame(VRSession::FramePhase::Draw)->mShouldRender) + { + mSwapchain[0]->beginFrame(info.getState()->getGraphicsContext()); + mSwapchain[1]->beginFrame(info.getState()->getGraphicsContext()); + } + mViewer->getCamera()->setViewport(0, 0, mFramebuffer->width(), mFramebuffer->height()); + + osg::GraphicsOperation* graphicsOperation = info.getCurrentCamera()->getRenderer(); + osgViewer::Renderer* renderer = dynamic_cast(graphicsOperation); + if (renderer != nullptr) + { + // Disable normal OSG FBO camera setup + renderer->setCameraRequiresSetUp(false); + } + + mRenderingReady = true; + } + + void VRViewer::preDrawCallback(osg::RenderInfo& info) + { + if (Environment::get().getSession()->getFrame(VRSession::FramePhase::Draw)->mShouldRender) + { + mFramebuffer->bindFramebuffer(info.getState()->getGraphicsContext(), GL_FRAMEBUFFER_EXT); + //mSwapchain->framebuffer()->bindFramebuffer(info.getState()->getGraphicsContext(), GL_FRAMEBUFFER_EXT); + } + } + + void VRViewer::postDrawCallback(osg::RenderInfo& info) + { + auto* camera = info.getCurrentCamera(); + auto name = camera->getName(); + } + + void VRViewer::finalDrawCallback(osg::RenderInfo& info) + { + auto* session = Environment::get().getSession(); + auto* frameMeta = session->getFrame(VRSession::FramePhase::Draw).get(); + + if (frameMeta->mShouldSyncFrameLoop) + { + if (frameMeta->mShouldRender) + { + blit(info); + } + } + } + + void VRViewer::swapBuffersCallback(osg::GraphicsContext* gc) + { + auto* session = Environment::get().getSession(); + session->swapBuffers(gc, *this); + mRenderingReady = false; + } + + void VRViewer::updateView(Misc::View& left, Misc::View& right) + { + auto phase = VRSession::FramePhase::Update; + auto session = Environment::get().getSession(); + + std::array views; + MWVR::Environment::get().getTrackingManager()->updateTracking(); + + auto& frame = session->getFrame(phase); + if (frame->mShouldRender) + { + left = frame->mViews[(int)ReferenceSpace::VIEW][(int)Side::LEFT_SIDE]; + left.pose.position *= Constants::UnitsPerMeter * session->playerScale(); + right = frame->mViews[(int)ReferenceSpace::VIEW][(int)Side::RIGHT_SIDE]; + right.pose.position *= Constants::UnitsPerMeter * session->playerScale(); + } + } + + void VRViewer::UpdateViewCallback::updateView(Misc::View& left, Misc::View& right) + { + mViewer->updateView(left, right); + } + + void VRViewer::FinaldrawCallback::operator()(osg::RenderInfo& info, Misc::StereoView::StereoDrawCallback::View view) const + { + if (view != Misc::StereoView::StereoDrawCallback::View::Left) + mViewer->finalDrawCallback(info); + } +} diff --git a/apps/openmw/mwvr/vrviewer.hpp b/apps/openmw/mwvr/vrviewer.hpp new file mode 100644 index 000000000..88f8803d5 --- /dev/null +++ b/apps/openmw/mwvr/vrviewer.hpp @@ -0,0 +1,164 @@ +#ifndef MWVR_VRVIEWER_H +#define MWVR_VRVIEWER_H + +#include +#include +#include + +#include +#include +#include + +#include "openxrmanager.hpp" + +#include +#include + +namespace MWVR +{ + class VRFramebuffer; + class VRView; + class OpenXRSwapchain; + + /// \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: + struct UpdateViewCallback : public Misc::StereoView::UpdateViewCallback + { + UpdateViewCallback(VRViewer* viewer) : mViewer(viewer) {}; + + //! Called during the update traversal of every frame to source updated stereo values. + virtual void updateView(Misc::View& left, Misc::View& right) override; + + VRViewer* mViewer; + }; + + class SwapBuffersCallback : public osg::GraphicsContext::SwapCallback + { + public: + SwapBuffersCallback(VRViewer* viewer) : mViewer(viewer) {}; + void swapBuffersImplementation(osg::GraphicsContext* gc) override; + + private: + VRViewer* mViewer; + }; + + class PredrawCallback : public osg::Camera::DrawCallback + { + public: + PredrawCallback(VRViewer* viewer) + : mViewer(viewer) + {} + + void operator()(osg::RenderInfo& info) const override { mViewer->preDrawCallback(info); }; + + private: + + VRViewer* mViewer; + }; + + class PostdrawCallback : public osg::Camera::DrawCallback + { + public: + PostdrawCallback(VRViewer* viewer) + : mViewer(viewer) + {} + + void operator()(osg::RenderInfo& info) const override { mViewer->postDrawCallback(info); }; + + private: + + VRViewer* mViewer; + }; + + class InitialDrawCallback : public osg::Camera::DrawCallback + { + public: + InitialDrawCallback(VRViewer* viewer) + : mViewer(viewer) + {} + + void operator()(osg::RenderInfo& info) const override { mViewer->initialDrawCallback(info); }; + + private: + + VRViewer* mViewer; + }; + + class FinaldrawCallback : public Misc::StereoView::StereoDrawCallback + { + public: + FinaldrawCallback(VRViewer* viewer) + : mViewer(viewer) + {} + + void operator()(osg::RenderInfo& info, Misc::StereoView::StereoDrawCallback::View view) const override; + + private: + + VRViewer* mViewer; + }; + + enum class MirrorTextureEye + { + Left, + Right, + Both + }; + + public: + VRViewer( + osg::ref_ptr viewer); + + ~VRViewer(void); + + void swapBuffersCallback(osg::GraphicsContext* gc); + void initialDrawCallback(osg::RenderInfo& info); + void preDrawCallback(osg::RenderInfo& info); + void postDrawCallback(osg::RenderInfo& info); + void finalDrawCallback(osg::RenderInfo& info); + void blit(osg::RenderInfo& gc); + void configureXR(osg::GraphicsContext* gc); + void configureCallbacks(); + void setupMirrorTexture(); + void processChangedSettings(const std::set< std::pair >& changed); + void updateView(Misc::View& left, Misc::View& right); + + SubImage subImage(Side side); + + bool xrConfigured() { return mOpenXRConfigured; }; + bool callbacksConfigured() { return mCallbacksConfigured; }; + + private: + std::mutex mMutex{}; + bool mOpenXRConfigured{ false }; + bool mCallbacksConfigured{ false }; + + osg::ref_ptr mViewer = nullptr; + osg::ref_ptr mPreDraw{ nullptr }; + osg::ref_ptr mPostDraw{ nullptr }; + osg::ref_ptr mFinalDraw{ nullptr }; + std::shared_ptr mUpdateViewCallback{ nullptr }; + bool mRenderingReady{ false }; + + std::unique_ptr mMirrorTexture; + std::vector mMirrorTextureViews; + bool mMirrorTextureShouldBeCleanedUp{ false }; + bool mMirrorTextureEnabled{ false }; + bool mFlipMirrorTextureOrder{ false }; + MirrorTextureEye mMirrorTextureEye{ MirrorTextureEye::Both }; + + std::unique_ptr mFramebuffer; + std::unique_ptr mMsaaResolveTexture; + std::unique_ptr mGammaResolveTexture; + std::array, 2> mSwapchain; + std::array mSubImages; + std::array mSwapchainConfig; + }; +} + +#endif diff --git a/apps/openmw/mwvr/vrvirtualkeyboard.cpp b/apps/openmw/mwvr/vrvirtualkeyboard.cpp new file mode 100644 index 000000000..2004b411f --- /dev/null +++ b/apps/openmw/mwvr/vrvirtualkeyboard.cpp @@ -0,0 +1,274 @@ +#include "vrvirtualkeyboard.hpp" + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/statemanager.hpp" + +namespace MWVR +{ + VirtualKeyboardManager::VirtualKeyboardManager() + : mVk(new VrVirtualKeyboard) + { + + } + + void VirtualKeyboardManager::registerEditBox(MyGUI::EditBox* editBox) + { + IDelegate* onSetFocusDelegate = newDelegate(mVk.get(), &VrVirtualKeyboard::delegateOnSetFocus); + IDelegate* onLostFocusDelegate = newDelegate(mVk.get(), &VrVirtualKeyboard::delegateOnLostFocus); + editBox->eventKeySetFocus += onSetFocusDelegate; + editBox->eventKeyLostFocus += onLostFocusDelegate; + + mDelegates[editBox] = Delegates(onSetFocusDelegate, onLostFocusDelegate); + } + + void VirtualKeyboardManager::unregisterEditBox(MyGUI::EditBox* editBox) + { + auto it = mDelegates.find(editBox); + if (it != mDelegates.end()) + { + editBox->eventKeySetFocus -= it->second.first; + editBox->eventKeyLostFocus -= it->second.second; + mDelegates.erase(it); + } + } + + + static const char* mClassTypeName; + + VrVirtualKeyboard::VrVirtualKeyboard() + : WindowBase("openmw_vr_virtual_keyboard.layout") + , mButtonBox(nullptr) + , mTarget(nullptr) + , mButtons() + , mShift(false) + , mCaps(false) + { + getWidget(mButtonBox, "ButtonBox"); + mMainWidget->setNeedKeyFocus(false); + mButtonBox->setNeedKeyFocus(false); + updateMenu(); + } + + VrVirtualKeyboard::~VrVirtualKeyboard() + { + + } + + void VrVirtualKeyboard::onResChange(int w, int h) + { + updateMenu(); + } + + void VrVirtualKeyboard::onFrame(float dt) + { + } + + void VrVirtualKeyboard::open(MyGUI::EditBox* target) + { + updateMenu(); + + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(target); + mTarget = target; + setVisible(true); + } + + void VrVirtualKeyboard::close() + { + setVisible(false); + mTarget = nullptr; + } + + void VrVirtualKeyboard::delegateOnSetFocus(MyGUI::Widget* _sender, MyGUI::Widget* _old) + { + if (_sender->getUserString("VirtualKeyboard") != "false") + open(static_cast(_sender)); + } + + void VrVirtualKeyboard::delegateOnLostFocus(MyGUI::Widget* _sender, MyGUI::Widget* _new) + { + close(); + } + + void VrVirtualKeyboard::onButtonClicked(MyGUI::Widget* sender) + { + assert(mTarget); + MyGUI::InputManager::getInstance().setKeyFocusWidget(mTarget); + + std::string name = *sender->getUserData(); + + if (name == "Esc") + onEsc(); + if (name == "Tab") + onTab(); + if (name == "Caps") + onCaps(); + if (name == "Shift") + onShift(); + else + mShift = false; + if (name == "Back") + onBackspace(); + if (name == "Return") + onReturn(); + if (name == "Space") + textInput(" "); + if (name == "->") + textInput("->"); + if (name.length() == 1) + textInput(name); + + updateMenu(); + } + + void VrVirtualKeyboard::textInput(const std::string& symbol) + { + MyGUI::UString ustring(symbol); + MyGUI::UString::utf32string utf32string = ustring.asUTF32(); + for (MyGUI::UString::utf32string::const_iterator it = utf32string.begin(); it != utf32string.end(); ++it) + MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::None, *it); + } + + void VrVirtualKeyboard::onEsc() + { + close(); + } + + void VrVirtualKeyboard::onTab() + { + MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Tab); + MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Tab); + } + + void VrVirtualKeyboard::onCaps() + { + mCaps = !mCaps; + } + + void VrVirtualKeyboard::onShift() + { + mShift = !mShift; + } + + void VrVirtualKeyboard::onBackspace() + { + MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Backspace); + MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Backspace); + } + + void VrVirtualKeyboard::onReturn() + { + MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::Return); + MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::Return); + } + + bool VrVirtualKeyboard::exit() + { + close(); + return true; + } + + void VrVirtualKeyboard::updateMenu() + { + // TODO: Localization? + static std::vector row1{ "`", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=", "Back" }; + static std::vector row2{ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "[", "]", "Return" }; + static std::vector row3{ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "'", "\\", "->" }; + static std::vector row4{ "Shift", "z", "x", "c", "v", "b", "n", "m", ",", ".", "/", "Space" }; + std::map shiftMap; + shiftMap["1"] = "!"; + shiftMap["2"] = "@"; + shiftMap["3"] = "#"; + shiftMap["4"] = "$"; + shiftMap["5"] = "%"; + shiftMap["6"] = "^"; + shiftMap["7"] = "&"; + shiftMap["8"] = "*"; + shiftMap["9"] = "("; + shiftMap["0"] = ")"; + shiftMap["-"] = "_"; + shiftMap["="] = "+"; + shiftMap["\\"] = "|"; + shiftMap[","] = "<"; + shiftMap["."] = ">"; + shiftMap["/"] = "?"; + shiftMap[";"] = ":"; + shiftMap["'"] = "\""; + shiftMap["["] = "{"; + shiftMap["]"] = "}"; + shiftMap["`"] = "~"; + + std::vector< std::vector< std::string > > rows{ row1, row2, row3, row4 }; + + int sideSize = 50; + int margin = 10; + int xmax = 0; + int ymax = 0; + + if (mButtons.empty()) + { + int y = margin; + for (auto& row : rows) + { + int x = margin; + for (std::string& buttonId : row) + { + int width = sideSize + 10 * (buttonId.length() - 1); + MyGUI::Button* button = mButtonBox->createWidget( + "MW_Button", MyGUI::IntCoord(x, y, width, sideSize), MyGUI::Align::Default, buttonId); + button->eventMouseButtonClick += MyGUI::newDelegate(this, &VrVirtualKeyboard::onButtonClicked); + button->setUserData(std::string(buttonId)); + button->setVisible(true); + button->setFontHeight(32); + button->setCaption(buttonId); + button->setNeedKeyFocus(false); + mButtons[buttonId] = button; + x += width + margin; + } + y += sideSize + margin; + } + } + + for (auto& row : rows) + { + for (std::string& buttonId : row) + { + auto* button = mButtons[buttonId]; + xmax = std::max(xmax, button->getAbsoluteRect().right); + ymax = std::max(ymax, button->getAbsoluteRect().bottom); + + if (buttonId.length() == 1) + { + auto caption = buttonId; + if (mShift ^ mCaps) + caption[0] = std::toupper(caption[0]); + else + caption[0] = std::tolower(caption[0]); + button->setCaption(caption); + button->setUserData(caption); + } + + if (mShift) + { + auto it = shiftMap.find(buttonId); + if (it != shiftMap.end()) + { + button->setCaption(it->second); + button->setUserData(it->second); + } + } + } + } + + std::cout << xmax << ", " << ymax << std::endl; + + setCoord(0, 0, xmax + margin, ymax + margin); + mButtonBox->setCoord(0, 0, xmax + margin, ymax + margin); + //mButtonBox->setCoord (margin, margin, width, height); + mButtonBox->setVisible(true); + } +} diff --git a/apps/openmw/mwvr/vrvirtualkeyboard.hpp b/apps/openmw/mwvr/vrvirtualkeyboard.hpp new file mode 100644 index 000000000..c2113c90c --- /dev/null +++ b/apps/openmw/mwvr/vrvirtualkeyboard.hpp @@ -0,0 +1,80 @@ +#ifndef OPENMW_GAME_MWVR_VRVIRTUALKEYBOARD_H +#define OPENMW_GAME_MWVR_VRVIRTUALKEYBOARD_H + +#include "../mwgui/windowbase.hpp" + +#include +#include "components/widgets/virtualkeyboardmanager.hpp" + +#include +#include + +namespace Gui +{ + class VirtualKeyboardManager; +} + +namespace MWVR +{ + class VrVirtualKeyboard : public MWGui::WindowBase + { + public: + + VrVirtualKeyboard(); + ~VrVirtualKeyboard(); + + void onResChange(int w, int h) override; + + void onFrame(float dt) override; + + bool exit() override; + + void open(MyGUI::EditBox* target); + + void close(); + + void delegateOnSetFocus(MyGUI::Widget* _sender, MyGUI::Widget* _old); + void delegateOnLostFocus(MyGUI::Widget* _sender, MyGUI::Widget* _old); + + private: + void onButtonClicked(MyGUI::Widget* sender); + void textInput(const std::string& symbol); + void onEsc(); + void onTab(); + void onCaps(); + void onShift(); + void onBackspace(); + void onReturn(); + void updateMenu(); + + MyGUI::Widget* mButtonBox; + MyGUI::EditBox* mTarget; + std::map mButtons; + bool mShift; + bool mCaps; + }; + + class VirtualKeyboardManager : public Gui::VirtualKeyboardManager + { + public: + VirtualKeyboardManager(); + + void registerEditBox(MyGUI::EditBox* editBox) override; + void unregisterEditBox(MyGUI::EditBox* editBox) override; + VrVirtualKeyboard& virtualKeyboard() { return *mVk; }; + + private: + std::unique_ptr mVk; + + // MyGUI deletes delegates when you remove them from an event. + // Therefore i need one pair of delegates per box instead of being able to reuse one pair. + // And i have to set them aside myself to know what to remove from each event. + // There is an IDelegateUnlink type that might simplify this, but it is poorly documented. + using IDelegate = MyGUI::EventHandle_WidgetWidget::IDelegate; + // .first = onSetFocus, .second = onLostFocus + using Delegates = std::pair; + std::map mDelegates; + }; +} + +#endif diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 63eb20a17..ce685003c 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -121,7 +121,7 @@ namespace MWWorld throw std::runtime_error ("class does not have item health"); } - void Class::hit(const Ptr& ptr, float attackStrength, int type) const + bool Class::hit(const Ptr& ptr, float attackStrength, int type, bool simulated) const { throw std::runtime_error("class cannot hit"); } @@ -131,7 +131,7 @@ namespace MWWorld throw std::runtime_error("class cannot block"); } - void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const + void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, float hitStrength) const { throw std::runtime_error("class cannot be hit"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 621ebb4fc..5789bf040 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -121,19 +121,22 @@ namespace MWWorld ///< Return item max health or throw an exception, if class does not have item health /// (default implementation: throw an exception) - virtual void hit(const Ptr& ptr, float attackStrength, int type=-1) const; + virtual bool hit(const Ptr& ptr, float attackStrength, int type, bool simulated = false ) const; ///< Execute a melee hit, using the current weapon. This will check the relevant skills /// of the given attacker, and whoever is hit. /// \param attackStrength how long the attack was charged for, a value in 0-1 range. /// \param type - type of attack, one of the MWMechanics::CreatureStats::AttackType /// enums. ignored for creature attacks. + /// \param simulated - If true, this function will only check if a hit would be made, and have no side effects. This parameter has no effect for Creature classes. + /// @return True if the attack had a victim, regardless if hit was successful or not. /// (default implementation: throw an exception) - virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const; + virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful, float hitStrength = 0.f) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the /// actor responsible for the attack, and \a successful specifies if the hit is - /// successful or not. + /// successful or not. \a hitStrength is the fraction of max attack strength applied, and is + /// used to determine haptic feedback intensity. virtual void block (const Ptr& ptr) const; ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 8bd91dfd6..779842d83 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -235,6 +235,11 @@ namespace MWWorld return ptr.getClass().getNpcStats(ptr).getDrawState(); } + void Player::activate(MWWorld::Ptr obj) + { + MWBase::Environment::get().getWorld()->activate(obj, getPlayer()); + } + void Player::activate() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) @@ -314,6 +319,18 @@ namespace MWWorld return MWBase::Environment::get().getMechanicsManager()->getActorsFighting(getPlayer()).size() != 0; } + bool Player::isDisabled() + { + bool disabled = false; + auto ptr = getPlayer(); + const MWWorld::Class& cls = ptr.getClass(); + auto& stats = cls.getCreatureStats(ptr); + disabled |= stats.getKnockedDown(); + disabled |= stats.getMagicEffects().get(ESM::MagicEffect::Paralyze).getMagnitude() > 0.f; + disabled |= stats.isDead(); + return disabled; + } + bool Player::enemiesNearby() { return MWBase::Environment::get().getMechanicsManager()->getEnemiesNearby(getPlayer()).size() != 0; diff --git a/apps/openmw/mwworld/player.hpp b/apps/openmw/mwworld/player.hpp index 1e4b0ffdf..7f1c01408 100644 --- a/apps/openmw/mwworld/player.hpp +++ b/apps/openmw/mwworld/player.hpp @@ -93,6 +93,9 @@ namespace MWWorld /// Activate the object under the crosshair, if any void activate(); + /// Activate a specific object + void activate(MWWorld::Ptr obj); + bool getAutoMove() const; void setAutoMove (bool enable); @@ -120,6 +123,9 @@ namespace MWWorld ///Checks all nearby actors to see if anyone has an aipackage against you bool isInCombat(); + ///Checks if the player is currently in a state where he cannot act + bool isDisabled(); + bool enemiesNearby(); void clear(); diff --git a/apps/openmw/mwworld/projectilemanager.cpp b/apps/openmw/mwworld/projectilemanager.cpp index 753ee69e8..077a46220 100644 --- a/apps/openmw/mwworld/projectilemanager.cpp +++ b/apps/openmw/mwworld/projectilemanager.cpp @@ -47,6 +47,11 @@ #include "../mwphysics/physicssystem.hpp" #include "../mwphysics/projectile.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vranimation.hpp" +#endif + namespace { ESM::EffectList getMagicBoltData(std::vector& projectileIDs, std::set& sounds, float& speed, std::string& texture, std::string& sourceName, const std::string& id) @@ -272,6 +277,15 @@ namespace MWWorld return; osg::Quat orient; +#ifdef USE_OPENXR + if (caster == MWBase::Environment::get().getWorld()->getPlayerPtr()) + { + osg::Matrix worldMatrix = MWVR::Environment::get().getPlayerAnimation()->getWeaponTransformMatrix(); + orient = worldMatrix.getRotate(); + pos = worldMatrix.getTrans(); + } + else +#endif if (caster.getClass().isActor()) orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 99fab075d..e6e591e58 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -48,6 +48,7 @@ #include #include +#include #include #include @@ -97,6 +98,14 @@ #include "contentloader.hpp" #include "esmloader.hpp" +#ifdef USE_OPENXR +#include "../mwvr/vranimation.hpp" +#include "../mwvr/vrenvironment.hpp" +#include "../mwvr/vrinputmanager.hpp" +#include "../mwvr/vrpointer.hpp" +#include "../mwvr/vrutil.hpp" +#endif + namespace { @@ -160,6 +169,7 @@ namespace MWWorld World::World ( osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + std::unique_ptr camera, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const Files::Collections& fileCollections, const std::vector& contentFiles, @@ -221,7 +231,7 @@ namespace MWWorld mNavigator.reset(new DetourNavigator::NavigatorStub()); } - mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, resourcePath, *mNavigator)); + mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, std::move(camera), resourceSystem, workQueue, resourcePath, *mNavigator)); mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get())); mRendering->preloadCommonAssets(); @@ -612,6 +622,7 @@ namespace MWWorld void World::useDeathCamera() { +#ifndef USE_OPENXR if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() ) { mRendering->getCamera()->togglePreviewMode(false); @@ -619,6 +630,7 @@ namespace MWWorld } if(mRendering->getCamera()->isFirstPerson()) mRendering->getCamera()->toggleViewMode(true); +#endif } MWWorld::Player& World::getPlayer() @@ -1112,6 +1124,12 @@ namespace MWWorld mWorldScene->changeToInteriorCell(cellName, position, adjustPlayerPos, changeEvent); addContainerScripts(getPlayerPtr(), getPlayerPtr().getCell()); mRendering->getCamera()->instantTransition(); + +#ifdef USE_OPENXR + auto* xrInput = MWVR::Environment::get().getInputManager(); + if (xrInput) + xrInput->requestRecenter(false); +#endif } void World::changeToExteriorCell (const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) @@ -1129,6 +1147,12 @@ namespace MWWorld mWorldScene->changeToExteriorCell(position, adjustPlayerPos, changeEvent); addContainerScripts(getPlayerPtr(), getPlayerPtr().getCell()); mRendering->getCamera()->instantTransition(); + +#ifdef USE_OPENXR + auto* xrInput = MWVR::Environment::get().getInputManager(); + if (xrInput) + xrInput->requestRecenter(false); +#endif } void World::changeToCell (const ESM::CellId& cellId, const ESM::Position& position, bool adjustPlayerPos, bool changeEvent) @@ -1160,6 +1184,10 @@ namespace MWWorld MWWorld::Ptr World::getFacedObject() { +#ifdef USE_OPENXR + return getPointerTarget(); +#endif + MWWorld::Ptr facedObject; if (MWBase::Environment::get().getWindowManager()->isGuiMode() && @@ -1211,15 +1239,25 @@ namespace MWWorld // the origin of hitbox is an actor's front, not center distance += halfExtents.y(); - // special cased for better aiming with the camera - // if we do not hit anything, will use the default approach as fallback if (ptr == getPlayerPtr()) { +#ifdef USE_OPENXR + // Use current aim of weapon to impact + osg::Matrix worldMatrix = MWVR::Environment::get().getPlayerAnimation()->getWeaponTransformMatrix(); + + auto result = mPhysics->getHitContact(ptr, worldMatrix.getTrans(), worldMatrix.getRotate(), distance, targets); + if (!result.first.isEmpty()) + Log(Debug::Verbose) << "Hit: " << result.first.getTypeName(); + return result; +#else + // special cased for better aiming with the camera + // if we do not hit anything, will use the default approach as fallback osg::Vec3f pos = getActorHeadTransform(ptr).getTrans(); std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); if(!result.first.isEmpty()) return std::make_pair(result.first, result.second); +#endif } osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); @@ -2264,7 +2302,7 @@ namespace MWWorld const float camDist = mRendering->getCamera()->getCameraDistance(); maxDistance += camDist; MWWorld::Ptr facedObject; - MWRender::RenderingManager::RayResult rayToObject; + MWRender::RayResult rayToObject; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { @@ -2494,7 +2532,7 @@ namespace MWWorld { const float maxDist = 200.f; - MWRender::RenderingManager::RayResult result = mRendering->castCameraToViewportRay(cursorX, cursorY, maxDist, true, true); + MWRender::RayResult result = mRendering->castCameraToViewportRay(cursorX, cursorY, maxDist, true, true); CellStore* cell = getPlayerPtr().getCell(); @@ -2521,7 +2559,7 @@ namespace MWWorld bool World::canPlaceObject(float cursorX, float cursorY) { const float maxDist = 200.f; - MWRender::RenderingManager::RayResult result = mRendering->castCameraToViewportRay(cursorX, cursorY, maxDist, true, true); + MWRender::RayResult result = mRendering->castCameraToViewportRay(cursorX, cursorY, maxDist, true, true); if (result.mHit) { @@ -2607,7 +2645,7 @@ namespace MWWorld float len = 1000000.0; - MWRender::RenderingManager::RayResult result = mRendering->castRay(orig, orig+dir*len, true, true); + MWRender::RayResult result = mRendering->castRay(orig, orig+dir*len, true, true); if (result.mHit) pos.pos[2] = result.mHitPointWorld.z(); @@ -2770,7 +2808,7 @@ namespace MWWorld if(!mRendering->getCamera()->isVanityOrPreviewModeEnabled()) return false; - mRendering->getCamera()->rotateCamera(rot[0], rot[2], true); + mRendering->getCamera()->rotateCamera(rot[0], 0.f, rot[2], true); return true; } @@ -3556,8 +3594,14 @@ namespace MWWorld // for player we can take faced object first MWWorld::Ptr target; +#ifdef USE_OPENXR + if (actor == MWMechanics::getPlayer()) + target = MWVR::Util::getTouchTarget().first; +#else + // Does not apply to VR if (actor == MWMechanics::getPlayer()) target = getFacedObject(); +#endif // if the faced object can not be activated, do not use it if (!target.isEmpty() && !target.getClass().hasToolTip(target)) @@ -3594,11 +3638,22 @@ namespace MWWorld osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); +#ifdef USE_OPENXR + if (actor == MWMechanics::getPlayer()) + { + osg::Matrix worldMatrix = MWVR::Environment::get().getPlayerAnimation()->getWeaponTransformMatrix(); + origin = worldMatrix.getTrans(); + orient = worldMatrix.getRotate(); + } +#endif + Log(Debug::Verbose) << "Origin: " << origin; + Log(Debug::Verbose) << "Orient: " << orient; + osg::Vec3f direction = orient * osg::Vec3f(0,1,0); float distance = getMaxActivationDistance(); osg::Vec3f dest = origin + direction * distance; - MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + MWRender::RayResult result2 = mRendering->castRay(origin, dest, true, true); float dist1 = std::numeric_limits::max(); float dist2 = std::numeric_limits::max(); @@ -4084,6 +4139,11 @@ namespace MWWorld return mPlayer->getConstPlayer(); } + MWRender::RenderingManager& World::getRenderingManager() + { + return *mRendering; + } + void World::updateDialogueGlobals() { MWWorld::Ptr player = getPlayerPtr(); @@ -4506,6 +4566,96 @@ namespace MWWorld return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); } + float World::getTargetObject(MWRender::RayResult& result, const osg::Vec3f& origin, const osg::Quat& orientation, float maxDistance, bool ignorePlayer) + { + osg::Vec3f direction = orientation * osg::Vec3f(0, 1, 0); + direction.normalize(); + osg::Vec3f end = origin + direction * maxDistance; + result = mRendering->castRay(origin, end, ignorePlayer); + if(!result.mHit) + return 0.f; + + MWWorld::Ptr facedObject = result.mHitObject; + if (facedObject.isEmpty() && result.mHitRefnum.hasContentFile()) + { + for (CellStore* cellstore : mWorldScene->getActiveCells()) + { + facedObject = cellstore->searchViaRefNum(result.mHitRefnum); + if (!facedObject.isEmpty()) break; + } + } + result.mHitObject = facedObject; + + return result.mRatio * maxDistance; + } + +#ifdef USE_OPENXR + MWVR::UserPointer& World::getUserPointer() + { + return mRendering->userPointer(); + } + + MWWorld::Ptr World::getPointerTarget() + { + return getUserPointer().getPointerTarget().mHitObject; + } +#endif + + MWWorld::Ptr World::placeObject(const MWWorld::ConstPtr& object, const MWRender::RayResult& ray, int amount) + { + const float maxDist = 200.f; + + CellStore* cell = getPlayerPtr().getCell(); + + ESM::Position pos = getPlayerPtr().getRefData().getPosition(); + + if (ray.mHit && !ray.mHitObject.isEmpty()) + { + pos.pos[0] = ray.mHitPointWorld.x(); + pos.pos[1] = ray.mHitPointWorld.y(); + pos.pos[2] = ray.mHitPointWorld.z(); + } + // We want only the Z part of the player's rotation + // TODO: Use the hand to orient in VR? + pos.rot[0] = 0; + pos.rot[1] = 0; + + // copy the object and set its count + MWWorld::Ptr dropped = copyObjectToCell(object, cell, pos, amount, true); + + // only the player place items in the world, so no need to check actor + PCDropped(dropped); + + return dropped; + } + + int World::getActiveWeaponType(void) + { + if (mPlayer) + { + if (mPlayer->getDrawState() == MWMechanics::DrawState_Nothing) + return ESM::Weapon::Type::None; + + if (mPlayer->getDrawState() == MWMechanics::DrawState_Spell) + return ESM::Weapon::Type::Spell; + + MWWorld::Ptr ptr = mPlayer->getPlayer(); + const MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); + MWWorld::ConstContainerStoreIterator it = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if (it != invStore.end()) + { + if (it->getTypeName() == typeid(ESM::Weapon).name()) + return ESM::Weapon::Type(it->get()->mBase->mData.mType); + if (it->getTypeName() == typeid(ESM::Lockpick).name()) + return ESM::Weapon::Type::PickProbe; + if (it->getTypeName() == typeid(ESM::Probe).name()) + return ESM::Weapon::Type::PickProbe; + } + return ESM::Weapon::Type::HandToHand; + } + return ESM::Weapon::Type::None; + } + bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const { return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index ad2e7c603..970bbd20c 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -7,6 +7,8 @@ #include "../mwbase/world.hpp" +#include "../mwrender/renderingmanager.hpp" + #include "ptr.hpp" #include "scene.hpp" #include "esmstore.hpp" @@ -185,7 +187,7 @@ namespace MWWorld const std::vector& content, const std::vector& groundcover, ContentLoader& contentLoader); float feetToGameUnits(float feet); - float getActivationDistancePlusTelekinesis(); + float getActivationDistancePlusTelekinesis() override; MWWorld::ConstPtr getClosestMarker( const MWWorld::Ptr &ptr, const std::string &id ); MWWorld::ConstPtr getClosestMarkerFromExteriorPosition( const osg::Vec3f& worldPos, const std::string &id ); @@ -198,6 +200,7 @@ namespace MWWorld World ( osgViewer::Viewer* viewer, osg::ref_ptr rootNode, + std::unique_ptr camera, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue, const Files::Collections& fileCollections, const std::vector& contentFiles, @@ -247,6 +250,8 @@ namespace MWWorld MWWorld::Ptr getPlayerPtr() override; MWWorld::ConstPtr getPlayerConstPtr() const override; + MWRender::RenderingManager& getRenderingManager() override; + const MWWorld::ESMStore& getStore() const override; /* @@ -920,6 +925,24 @@ namespace MWWorld bool hasCollisionWithDoor(const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; + /// Intersects the scene from the origin, in the specified orientation and distance, storing the %result in the result structure. + /// @Return distance to the target object, or -1 if no object was targeted / in range + float getTargetObject(MWRender::RayResult& result, const osg::Vec3f& origin, const osg::Quat& orientation, float maxDistance, bool ignorePlayer) override; + +#ifdef USE_OPENXR + MWVR::UserPointer& getUserPointer() override; + MWWorld::Ptr getPointerTarget() override; +#endif + + MWWorld::Ptr placeObject(const MWWorld::ConstPtr& object, const MWRender::RayResult& ray, int amount) override; + ///< copy and place an object into the gameworld based on the given intersection + /// @param object + /// @param world position to place object + /// @param number of objects to place + + /// @Return ESM::Weapon::Type enum describing the type of weapon currently drawn by the player. + int getActiveWeaponType(void) override; + bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, const MWWorld::ConstPtr& ignore) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; diff --git a/apps/openmw/vrengine.cpp b/apps/openmw/vrengine.cpp new file mode 100644 index 000000000..dd542520c --- /dev/null +++ b/apps/openmw/vrengine.cpp @@ -0,0 +1,22 @@ +#include "engine.hpp" + +#include "mwvr/openxrmanager.hpp" +#include "mwvr/vrsession.hpp" +#include "mwvr/vrviewer.hpp" +#include "mwvr/vrgui.hpp" +#include "mwvr/vrtracking.hpp" + +#ifndef USE_OPENXR +#error "USE_OPENXR not defined" +#endif + +void OMW::Engine::initVr() +{ + if (!mViewer) + throw std::logic_error("mViewer must be initialized before calling initVr()"); + + mXrEnvironment.setManager(new MWVR::OpenXRManager); + mXrEnvironment.setSession(new MWVR::VRSession()); + mXrEnvironment.setViewer(new MWVR::VRViewer(mViewer)); + mXrEnvironment.setTrackingManager(new MWVR::VRTrackingManager()); +} diff --git a/cmake/FindOpenXR.cmake b/cmake/FindOpenXR.cmake new file mode 100644 index 000000000..ece70d70d --- /dev/null +++ b/cmake/FindOpenXR.cmake @@ -0,0 +1,29 @@ +# Locate OpenXR library +# This module defines +# OpenXR_LIBRARY, the OpenXR library, with no other libraries +# OpenXR_LIBRARIES, the OpenXR library and required components with compiler flags +# OpenXR_FOUND, if false, do not try to link to OpenXR +# OpenXR_INCLUDE_DIR, where to find openxr.h +# OpenXR_VERSION, the version of the found library +# + + +if(WIN32) + if (CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_openxr_default_folder "C:/Program Files/OPENXR") + else() + set(_openxr_default_folder "C:/Program Files (x86)/OPENXR") + endif() +endif(WIN32) + +libfind_pkg_detect(OpenXR openxr + FIND_PATH openxr/openxr.h + HINTS $ENV{OPENXR_ROOT} ${_openxr_default_folder} + PATH_SUFFIXES include + FIND_LIBRARY openxr_loader.lib + HINTS $ENV{OPENXR_ROOT} ${_openxr_default_folder} + PATH_SUFFIXES lib lib32 lib64 +) +libfind_version_n_header(OpenXR NAMES openxr/openxr.h DEFINES XR_VERSION_MAJOR XR_VERSION_MINOR XR_VERSION_PATCH) + +libfind_process(OpenXR) diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index d70b6cb9d..b94a286f7 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -220,3 +220,15 @@ macro (copy_all_resource_files source_dir destination_dir_base destination_dir_r copy_resource_file("${source_dir}/${f}" "${destination_dir_base}" "${destination_dir_relative}/${filename}") endforeach (f) endmacro (copy_all_resource_files) + +if(NOT ${CMAKE_VERSION} VERSION_LESS 3.11) + if(${CMAKE_VERSION} VERSION_LESS 3.14) + macro(FetchContent_MakeAvailable NAME) + FetchContent_GetProperties(${NAME}) + if(NOT ${NAME}_POPULATED) + FetchContent_Populate(${NAME}) + add_subdirectory(${${NAME}_SOURCE_DIR} ${${NAME}_BINARY_DIR}) + endif() + endmacro() + endif() +endif() \ No newline at end of file diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 2b56aa318..2ceb17419 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -35,7 +35,7 @@ add_component_dir (settings # Start of tes3mp change # # Don't include certain components in server-only builds -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change add_component_dir (bsa bsa_file compressedbsafile memorystream @@ -57,7 +57,7 @@ add_component_dir (shader add_component_dir (sceneutil clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer - actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller + actorutil detourdebugdraw navmesh agentpath shadow mwshadowtechnique recastmesh shadowsbin osgacontroller rtt ) add_component_dir (nif @@ -74,7 +74,7 @@ add_component_dir (nifbullet # Start of tes3mp change # # Don't include certain components in server-only builds -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change add_component_dir (to_utf8 @@ -95,7 +95,7 @@ add_component_dir (esm # Start of tes3mp change # # Don't include certain components in server-only builds -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change add_component_dir (esmterrain storage @@ -103,11 +103,11 @@ add_component_dir (esmterrain # Start of tes3mp change # # Don't include certain components in server-only builds -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change add_component_dir (misc - constants utf8stream stringops resourcehelpers rng messageformatparser weakcache thread + constants utf8stream stringops resourcehelpers rng messageformatparser weakcache stereo callbackmanager thread ) add_component_dir (debug @@ -143,7 +143,7 @@ add_component_dir (translation # Start of tes3mp addition # # Don't include certain components in server-only builds -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change add_component_dir (terrain storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode viewdata cellborder @@ -158,7 +158,7 @@ add_component_dir (myguiplatform ) add_component_dir (widgets - box fontwrapper imagebutton tags list numericeditbox sharedstatebutton windowcaption widgets + box fontwrapper imagebutton tags list numericeditbox sharedstatebutton virtualkeyboardmanager windowcaption widgets ) add_component_dir (fontloader @@ -171,7 +171,7 @@ add_component_dir (sdlutil # Start of tes3mp change # # Don't include certain components in server-only builds -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change add_component_dir (version @@ -255,7 +255,7 @@ add_component_dir (fallback # # Don't require the crashcatcher when building on platforms other than Windows or when building only the server, # as it causes compilation problems -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) if(WIN32) add_component_dir (crashcatcher windows_crashcatcher @@ -263,13 +263,13 @@ IF (BUILD_OPENMW OR BUILD_OPENCS) windows_crashshm ) endif() -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change (major) # Start of tes3mp change (major) # # Don't require the DetourNavigator when building the server -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) add_component_dir(detournavigator debug makenavmesh @@ -292,7 +292,7 @@ IF (BUILD_OPENMW OR BUILD_OPENCS) oscillatingrecastmeshobject offmeshconnectionsmanager ) -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change (major) set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui @@ -301,7 +301,7 @@ set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui # Start of tes3mp change (major) # # Don't require Qt when building the server -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change if (USE_QT) add_component_qt_dir (contentselector @@ -330,7 +330,7 @@ endif() # Start of tes3mp change (major) # # Don't require Qt when building the server -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") @@ -346,7 +346,7 @@ add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) # Start of tes3mp change (major) # # Don't require graphics related libs when building the server -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) target_link_libraries(components # CMake's built-in OSG finder does not use pkgconfig, so we have to # manually ensure the order is correct for inter-library dependencies. @@ -368,7 +368,7 @@ target_link_libraries(components ${MyGUI_LIBRARIES} LZ4::LZ4 ) -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) target_link_libraries(components ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} @@ -380,7 +380,7 @@ target_link_libraries(components # Start of tes3mp change (major) # # Don't require RecastNavigation nor Bullet when building the server -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) target_link_libraries(components RecastNavigation::DebugUtils RecastNavigation::Detour @@ -388,7 +388,7 @@ IF (BUILD_OPENMW OR BUILD_OPENCS) ${BULLET_LIBRARIES} ) -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) target_link_libraries(components Base64) # End of tes3mp change (major) @@ -401,7 +401,7 @@ endif() # Start of tes3mp change (major) # # Don't require Qt when building the server -IF (BUILD_OPENMW OR BUILD_OPENCS) +IF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change if (USE_QT) target_link_libraries(components Qt5::Widgets Qt5::Core) @@ -409,7 +409,7 @@ endif() # Start of tes3mp change (major) # # Don't require Qt when building the server -ENDIF (BUILD_OPENMW OR BUILD_OPENCS) +ENDIF (BUILD_OPENMW OR BUILD_OPENCS OR BUILD_OPENMW_VR) # End of tes3mp change if (GIT_CHECKOUT) diff --git a/components/misc/callbackmanager.cpp b/components/misc/callbackmanager.cpp new file mode 100644 index 000000000..9b0c4cb6e --- /dev/null +++ b/components/misc/callbackmanager.cpp @@ -0,0 +1,122 @@ +#include "callbackmanager.hpp" + +namespace Misc +{ + + static CallbackManager* sInstance = nullptr; + + CallbackManager& CallbackManager::instance() + { + return *sInstance; + } + + struct InternalDrawCallback : public osg::Camera::DrawCallback + { + public: + InternalDrawCallback(CallbackManager* manager, CallbackManager::DrawStage stage) + : mManager(manager) + , mStage(stage) + {} + + void operator()(osg::RenderInfo& info) const override { mManager->callback(mStage, info); }; + + private: + + CallbackManager* mManager; + CallbackManager::DrawStage mStage; + }; + + CallbackManager::CallbackManager(osg::ref_ptr viewer) + : mInternalCallbacks{ } + , mUserCallbacks{ } + , mViewer{ viewer } + { + if (sInstance) + throw std::logic_error("Double instance og StereoView"); + sInstance = this; + + mInternalCallbacks[DrawStage::Initial] = new InternalDrawCallback(this, DrawStage::Initial); + mInternalCallbacks[DrawStage::PreDraw] = new InternalDrawCallback(this, DrawStage::PreDraw); + mInternalCallbacks[DrawStage::PostDraw] = new InternalDrawCallback(this, DrawStage::PostDraw); + mInternalCallbacks[DrawStage::Final] = new InternalDrawCallback(this, DrawStage::Final); + + auto* camera = mViewer->getCamera(); + camera->setInitialDrawCallback(mInternalCallbacks[DrawStage::Initial]); + camera->setPreDrawCallback(mInternalCallbacks[DrawStage::PreDraw]); + camera->setPostDrawCallback(mInternalCallbacks[DrawStage::PostDraw]); + camera->setFinalDrawCallback(mInternalCallbacks[DrawStage::Final]); + } + + void CallbackManager::callback(DrawStage stage, osg::RenderInfo& info) + { + std::unique_lock lock(mMutex); + auto frameNo = info.getState()->getFrameStamp()->getFrameNumber(); + auto& callbacks = mUserCallbacks[stage]; + + for (int i = 0; static_cast(i) < callbacks.size(); i++) + { + auto& callbackInfo = callbacks[i]; + if (frameNo >= callbackInfo.frame) + { + callbackInfo.callback->run(info); + if (callbackInfo.oneshot) + { + callbacks.erase(callbacks.begin() + 1); + i--; + } + } + } + + mCondition.notify_all(); + } + + void CallbackManager::addCallback(DrawStage stage, DrawCallback* cb) + { + std::unique_lock lock(mMutex); + CallbackInfo callbackInfo; + callbackInfo.callback = cb; + callbackInfo.frame = mViewer->getFrameStamp()->getFrameNumber(); + callbackInfo.oneshot = false; + mUserCallbacks[stage].push_back(callbackInfo); + } + + void CallbackManager::removeCallback(DrawStage stage, DrawCallback* cb) + { + std::unique_lock lock(mMutex); + auto& cbs = mUserCallbacks[stage]; + for (uint32_t i = 0; i < cbs.size(); i++) + if (cbs[i].callback == cb) + cbs.erase(cbs.begin() + i); + } + + void CallbackManager::addCallbackOneshot(DrawStage stage, DrawCallback* cb) + { + std::unique_lock lock(mMutex); + CallbackInfo callbackInfo; + callbackInfo.callback = cb; + callbackInfo.frame = mViewer->getFrameStamp()->getFrameNumber(); + callbackInfo.oneshot = true; + mUserCallbacks[stage].push_back(callbackInfo); + } + + void CallbackManager::waitCallbackOneshot(DrawStage stage, DrawCallback* cb) + { + std::unique_lock lock(mMutex); + while (hasOneshot(stage, cb)) + mCondition.wait(lock); + } + + void CallbackManager::beginFrame() + { + } + + bool CallbackManager::hasOneshot(DrawStage stage, DrawCallback* cb) + { + for (auto& callbackInfo : mUserCallbacks[stage]) + if (callbackInfo.callback == cb) + return true; + return false; + } + + +} diff --git a/components/misc/callbackmanager.hpp b/components/misc/callbackmanager.hpp new file mode 100644 index 000000000..bc16121a2 --- /dev/null +++ b/components/misc/callbackmanager.hpp @@ -0,0 +1,75 @@ +#ifndef MISC_CALLBACKMANAGER_H +#define MISC_CALLBACKMANAGER_H + +#include +#include + +#include +#include +#include +#include + +namespace Misc +{ + /// Manager of DrawCallbacks on an OSG camera. + /// The motivation behind this class is that OSG's draw callbacks are inherently thread unsafe. + /// Primarily, as the main thread has no way to synchronize with the draw thread when adding/removing draw callbacks, + /// when adding a draw callback the callback must manually compare frame numbers to make sure it doesn't fire for the + /// previous frame's draw traversals. This class automates this. + /// Secondly older versions of OSG that are still supported by openmw do not support nested callbacks. And the ones that do + /// make no attempt at synchronizing adding/removing nested callbacks, causing a potentially fatal race condition when needing + /// to dynamically add or remove a nested callback. + class CallbackManager + { + + public: + enum class DrawStage + { + Initial, PreDraw, PostDraw, Final + }; + + using DrawCallback = osg::Camera::DrawCallback; + struct CallbackInfo + { + osg::ref_ptr callback = nullptr; + unsigned int frame = 0; + bool oneshot = false; + }; + + static CallbackManager& instance(); + + CallbackManager(osg::ref_ptr viewer); + + /// Internal + void callback(DrawStage stage, osg::RenderInfo& info); + + /// Add a callback to a specific stage + void addCallback(DrawStage stage, DrawCallback* cb); + + /// Remove a callback from a specific stage + void removeCallback(DrawStage stage, DrawCallback* cb); + + /// Add a callback that will only fire once before being automatically removed. + void addCallbackOneshot(DrawStage stage, DrawCallback* cb); + + /// Waits for a oneshot callback to complete. Returns immediately if already complete or no such callback exists + void waitCallbackOneshot(DrawStage stage, DrawCallback* cb); + + /// + void beginFrame(); + + private: + + bool hasOneshot(DrawStage stage, DrawCallback* cb); + + std::map > mInternalCallbacks; + std::map > mUserCallbacks; + std::mutex mMutex; + std::condition_variable mCondition; + + uint32_t mFrame; + osg::ref_ptr mViewer; + }; +} + +#endif diff --git a/components/misc/stereo.cpp b/components/misc/stereo.cpp new file mode 100644 index 000000000..beecab152 --- /dev/null +++ b/components/misc/stereo.cpp @@ -0,0 +1,759 @@ +#include "stereo.hpp" +#include "stringops.hpp" +#include "callbackmanager.hpp" + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include + +#include +#include + +#include + +namespace Misc +{ + 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; + } + + osg::Matrix Pose::viewMatrix(bool useGLConventions) + { + if (useGLConventions) + { + // When applied as an offset to an existing view matrix, + // that view matrix will already convert points to a camera space + // with opengl conventions. So we need to convert offsets to opengl + // conventions. + 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; + } + else + { + 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; + } + } + + 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 + { + 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 View::operator==(const View& rhs) const + { + return pose == rhs.pose && fov == rhs.fov; + } + + std::ostream& operator <<( + std::ostream& os, + const Pose& pose) + { + os << "position=" << pose.position << ", orientation=" << pose.orientation; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const FieldOfView& fov) + { + os << "left=" << fov.angleLeft << ", right=" << fov.angleRight << ", down=" << fov.angleDown << ", up=" << fov.angleUp; + return os; + } + + std::ostream& operator <<( + std::ostream& os, + const View& view) + { + os << "pose=< " << view.pose << " >, fov=< " << view.fov << " >"; + return os; + } + + // Update stereo view/projection during update + class StereoUpdateCallback : public osg::Callback + { + public: + StereoUpdateCallback(StereoView* stereoView) : stereoView(stereoView) {} + + bool run(osg::Object* object, osg::Object* data) override + { + auto b = traverse(object, data); + stereoView->update(); + return b; + } + + StereoView* stereoView; + }; + + // Update states during cull + class StereoStatesetUpdateCallback : public SceneUtil::StateSetUpdater + { + public: + StereoStatesetUpdateCallback(StereoView* view) + : stereoView(view) + { + } + + protected: + virtual void setDefaults(osg::StateSet* stateset) + { + auto stereoViewMatrixUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "stereoViewMatrices", 2); + stateset->addUniform(stereoViewMatrixUniform, osg::StateAttribute::OVERRIDE); + auto stereoViewProjectionsUniform = new osg::Uniform(osg::Uniform::FLOAT_MAT4, "stereoViewProjections", 2); + stateset->addUniform(stereoViewProjectionsUniform); + auto geometryPassthroughUniform = new osg::Uniform("geometryPassthrough", false); + stateset->addUniform(geometryPassthroughUniform); + } + + virtual void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) + { + stereoView->updateStateset(stateset); + } + + private: + StereoView* stereoView; + }; + + static StereoView* sInstance = nullptr; + + StereoView& StereoView::instance() + { + return *sInstance; + } + + static osg::Camera* + createCamera(std::string name, GLbitfield clearMask) + { + auto* camera = new osg::Camera; + + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setProjectionResizePolicy(osg::Camera::FIXED); + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setViewMatrix(osg::Matrix::identity()); + camera->setName(name); + camera->setDataVariance(osg::Object::STATIC); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setClearMask(clearMask); + camera->setUpdateCallback(new SceneUtil::StateSetUpdater()); + + return camera; + } + + StereoView::StereoView(osg::Node::NodeMask noShaderMask, osg::Node::NodeMask sceneMask) + : mViewer(nullptr) + , mMainCamera(nullptr) + , mRoot(nullptr) + , mStereoRoot(new osg::Group) + , mUpdateCallback(new StereoUpdateCallback(this)) + , mTechnique(Technique::None) + , mNoShaderMask(noShaderMask) + , mSceneMask(sceneMask) + , mCullMask(0) + , mMasterConfig(new SharedShadowMapConfig) + , mSlaveConfig(new SharedShadowMapConfig) + , mSharedShadowMaps(Settings::Manager::getBool("shared shadow maps", "Stereo")) + , mUpdateViewCallback(new DefaultUpdateViewCallback) + { + mMasterConfig->_id = "STEREO"; + mMasterConfig->_master = true; + mSlaveConfig->_id = "STEREO"; + mSlaveConfig->_master = false; + + mStereoRoot->setName("Stereo Root"); + mStereoRoot->setDataVariance(osg::Object::STATIC); + mStereoRoot->addChild(mStereoGeometryShaderRoot); + mStereoRoot->addChild(mStereoBruteForceRoot); + mStereoRoot->addCullCallback(new StereoStatesetUpdateCallback(this)); + + if (sInstance) + throw std::logic_error("Double instance og StereoView"); + sInstance = this; + + auto* ds = osg::DisplaySettings::instance().get(); + ds->setStereo(true); + ds->setStereoMode(osg::DisplaySettings::StereoMode::HORIZONTAL_SPLIT); + ds->setUseSceneViewForStereoHint(true); + } + + void StereoView::initializeStereo(osgViewer::Viewer* viewer, Technique technique) + { + mViewer = viewer; + mRoot = viewer->getSceneData()->asGroup(); + mMainCamera = viewer->getCamera(); + mCullMask = mMainCamera->getCullMask(); + + setStereoTechnique(technique); + } + + void StereoView::initializeScene() + { + SceneUtil::FindByNameVisitor findScene("Scene Root"); + mRoot->accept(findScene); + mScene = findScene.mFoundNode; + if (!mScene) + throw std::logic_error("Couldn't find scene root"); + + if (mTechnique == Technique::GeometryShader_IndexedViewports) + { + mLeftCamera->addChild(mScene); // Use scene directly to avoid redundant shadow computation. + mRightCamera->addChild(mScene); + } + } + + void StereoView::setupBruteForceTechnique() + { + auto* ds = osg::DisplaySettings::instance().get(); + ds->setStereo(true); + ds->setStereoMode(osg::DisplaySettings::StereoMode::HORIZONTAL_SPLIT); + ds->setUseSceneViewForStereoHint(true); + + struct ComputeStereoMatricesCallback : public osgUtil::SceneView::ComputeStereoMatricesCallback + { + ComputeStereoMatricesCallback(StereoView* sv) + : mStereoView(sv) + { + + } + + osg::Matrixd computeLeftEyeProjection(const osg::Matrixd& projection) const override + { + return mStereoView->computeLeftEyeProjection(projection); + } + + osg::Matrixd computeLeftEyeView(const osg::Matrixd& view) const override + { + return mStereoView->computeLeftEyeView(view); + } + + osg::Matrixd computeRightEyeProjection(const osg::Matrixd& projection) const override + { + return mStereoView->computeRightEyeProjection(projection); + } + + osg::Matrixd computeRightEyeView(const osg::Matrixd& view) const override + { + return mStereoView->computeRightEyeView(view); + } + + StereoView* mStereoView; + }; + + auto* renderer = static_cast(mMainCamera->getRenderer()); + + // osgViewer::Renderer always has two scene views + for (auto* sceneView : { renderer->getSceneView(0), renderer->getSceneView(1) }) + { + sceneView->setComputeStereoMatricesCallback(new ComputeStereoMatricesCallback(this)); + + if (mSharedShadowMaps) + { + sceneView->getCullVisitorLeft()->setUserData(mMasterConfig); + sceneView->getCullVisitorRight()->setUserData(mSlaveConfig); + } + } + } + + void StereoView::setupGeometryShaderIndexedViewportTechnique() + { + mLeftCamera = createCamera("Stereo Left", GL_NONE); + mRightCamera = createCamera("Stereo Right", GL_NONE); + mStereoBruteForceRoot->addChild(mLeftCamera); + mStereoBruteForceRoot->addChild(mRightCamera); + + // Inject self as the root of the scene graph + mStereoGeometryShaderRoot->addChild(mRoot); + mViewer->setSceneData(mStereoRoot); + } + + static void removeSlave(osgViewer::Viewer* viewer, osg::Camera* camera) + { + for (unsigned int i = 0; i < viewer->getNumSlaves(); i++) + { + auto& slave = viewer->getSlave(i); + if (slave._camera == camera) + { + viewer->removeSlave(i); + return; + } + } + } + + void StereoView::removeBruteForceTechnique() + { + auto* ds = osg::DisplaySettings::instance().get(); + ds->setStereo(false); + if(mMainCamera->getUserData() == mMasterConfig) + mMainCamera->setUserData(nullptr); + } + + void StereoView::removeGeometryShaderIndexedViewportTechnique() + { + mStereoGeometryShaderRoot->removeChild(mRoot); + mViewer->setSceneData(mRoot); + mStereoBruteForceRoot->removeChild(mLeftCamera); + mStereoBruteForceRoot->removeChild(mRightCamera); + mLeftCamera = nullptr; + mRightCamera = nullptr; + } + + void StereoView::disableStereo() + { + if (mTechnique == Technique::None) + return; + + mMainCamera->removeUpdateCallback(mUpdateCallback); + + switch (mTechnique) + { + case Technique::GeometryShader_IndexedViewports: + removeGeometryShaderIndexedViewportTechnique(); break; + case Technique::BruteForce: + removeBruteForceTechnique(); break; + default: break; + } + + mMainCamera->setCullMask(mCullMask); + } + + void StereoView::enableStereo() + { + if (mTechnique == Technique::None) + return; + + // Update stereo statesets/matrices, but after the main camera updates. + auto mainCameraCB = mMainCamera->getUpdateCallback(); + mMainCamera->removeUpdateCallback(mainCameraCB); + mMainCamera->addUpdateCallback(mUpdateCallback); + mMainCamera->addUpdateCallback(mainCameraCB); + + switch (mTechnique) + { + case Technique::GeometryShader_IndexedViewports: + setupGeometryShaderIndexedViewportTechnique(); break; + case Technique::BruteForce: + setupBruteForceTechnique(); break; + default: break; + } + + setCullMask(mCullMask); + } + + void StereoView::setStereoTechnique(Technique technique) + { + if (technique == mTechnique) + return; + + disableStereo(); + mTechnique = technique; + enableStereo(); + } + + void StereoView::update() + { + View left{}; + View right{}; + double near_ = 1.f; + double far_ = 10000.f; + if (!mUpdateViewCallback) + { + Log(Debug::Error) << "StereoView: No update view callback. Stereo rendering will not work."; + return; + } + mUpdateViewCallback->updateView(left, right); + auto viewMatrix = mViewer->getCamera()->getViewMatrix(); + auto projectionMatrix = mViewer->getCamera()->getProjectionMatrix(); + near_ = Settings::Manager::getFloat("near clip", "Camera"); + far_ = Settings::Manager::getFloat("viewing distance", "Camera"); + + osg::Vec3d leftEye = left.pose.position; + osg::Vec3d rightEye = right.pose.position; + + osg::Matrix leftViewOffset = left.pose.viewMatrix(true); + osg::Matrix rightViewOffset = right.pose.viewMatrix(true); + + osg::Matrix leftViewMatrix = viewMatrix * leftViewOffset; + osg::Matrix rightViewMatrix = viewMatrix * rightViewOffset; + + osg::Matrix leftProjectionMatrix = left.fov.perspectiveMatrix(near_, far_); + osg::Matrix rightProjectionMatrix = right.fov.perspectiveMatrix(near_, far_); + + mRightCamera->setViewMatrix(rightViewMatrix); + mLeftCamera->setViewMatrix(leftViewMatrix); + mRightCamera->setProjectionMatrix(rightProjectionMatrix); + mLeftCamera->setProjectionMatrix(leftProjectionMatrix); + + auto width = mMainCamera->getViewport()->width(); + auto height = mMainCamera->getViewport()->height(); + + // To correctly cull when drawing stereo using the geometry shader, the main camera must + // draw a fake view+perspective that includes the full frustums of both the left and right eyes. + // This frustum will be computed as a perspective frustum from a position P slightly behind the eyes L and R + // where it creates the minimum frustum encompassing both eyes' frustums. + // NOTE: I make an assumption that the eyes lie in a horizontal plane relative to the base view, + // and lie mirrored around the Y axis (straight ahead). + // Re-think this if that turns out to be a bad assumption. + View frustumView; + + // Compute Frustum angles. A simple min/max. + /* Example values for reference: + Left: + angleLeft -0.767549932 float + angleRight 0.620896876 float + angleDown -0.837898076 float + angleUp 0.726982594 float + + Right: + angleLeft -0.620896876 float + angleRight 0.767549932 float + angleDown -0.837898076 float + angleUp 0.726982594 float + */ + frustumView.fov.angleLeft = std::min(left.fov.angleLeft, right.fov.angleLeft); + frustumView.fov.angleRight = std::max(left.fov.angleRight, right.fov.angleRight); + frustumView.fov.angleDown = std::min(left.fov.angleDown, right.fov.angleDown); + frustumView.fov.angleUp = std::max(left.fov.angleUp, right.fov.angleUp); + + // Check that the case works for this approach + auto maxAngle = std::max(frustumView.fov.angleRight - frustumView.fov.angleLeft, frustumView.fov.angleUp - frustumView.fov.angleDown); + if (maxAngle > osg::PI) + { + Log(Debug::Error) << "Total FOV exceeds 180 degrees. Case cannot be culled in single-pass VR. Disabling culling to cope. Consider switching to dual-pass VR."; + mMainCamera->setCullingActive(false); + return; + // TODO: An explicit frustum projection could cope, so implement that later. Guarantee you there will be VR headsets with total horizontal fov > 180 in the future. Maybe already. + } + + // Use the law of sines on the triangle spanning PLR to determine P + double angleLeft = std::abs(frustumView.fov.angleLeft); + double angleRight = std::abs(frustumView.fov.angleRight); + double lengthRL = (rightEye - leftEye).length(); + double ratioRL = lengthRL / std::sin(osg::PI - angleLeft - angleRight); + double lengthLP = ratioRL * std::sin(angleRight); + + osg::Vec3d directionLP = osg::Vec3(std::cos(-angleLeft), std::sin(-angleLeft), 0); + osg::Vec3d LP = directionLP * lengthLP; + frustumView.pose.position = leftEye + LP; + //frustumView.pose.position.x() += 1000; + + // Base view position is 0.0, by definition. + // The length of the vector P is therefore the required offset to near/far. + auto nearFarOffset = frustumView.pose.position.length(); + + // Generate the frustum matrices + auto frustumViewMatrix = viewMatrix * frustumView.pose.viewMatrix(true); + auto frustumProjectionMatrix = frustumView.fov.perspectiveMatrix(near_ + nearFarOffset, far_ + nearFarOffset); + + if (mTechnique == Technique::GeometryShader_IndexedViewports) + { + + // Update camera with frustum matrices + mMainCamera->setViewMatrix(frustumViewMatrix); + mMainCamera->setProjectionMatrix(frustumProjectionMatrix); + mLeftCamera->getOrCreateStateSet()->setAttribute(new osg::ViewportIndexed(0, 0, 0, width / 2, height), osg::StateAttribute::OVERRIDE); + mRightCamera->getOrCreateStateSet()->setAttribute(new osg::ViewportIndexed(0, width / 2, 0, width / 2, height), osg::StateAttribute::OVERRIDE); + } + else + { + mLeftCamera->setClearColor(mMainCamera->getClearColor()); + mRightCamera->setClearColor(mMainCamera->getClearColor()); + + mLeftCamera->setViewport(0, 0, width / 2, height); + mRightCamera->setViewport(width / 2, 0, width / 2, height); + + if (mMasterConfig->_projection == nullptr) + mMasterConfig->_projection = new osg::RefMatrix; + if (mMasterConfig->_modelView == nullptr) + mMasterConfig->_modelView = new osg::RefMatrix; + + if (mSharedShadowMaps) + { + mMasterConfig->_referenceFrame = mMainCamera->getReferenceFrame(); + mMasterConfig->_modelView->set(frustumViewMatrix); + mMasterConfig->_projection->set(projectionMatrix); + } + } + } + + void StereoView::updateStateset(osg::StateSet* stateset) + { + // Manage viewports in update to automatically catch window/resolution changes. + auto width = mMainCamera->getViewport()->width(); + auto height = mMainCamera->getViewport()->height(); + stateset->setAttribute(new osg::ViewportIndexed(0, 0, 0, width / 2, height)); + stateset->setAttribute(new osg::ViewportIndexed(1, width / 2, 0, width / 2, height)); + + // Update stereo uniforms + auto frustumViewMatrixInverse = osg::Matrix::inverse(mMainCamera->getViewMatrix()); + //auto frustumViewProjectionMatrixInverse = osg::Matrix::inverse(mMainCamera->getProjectionMatrix()) * osg::Matrix::inverse(mMainCamera->getViewMatrix()); + auto* stereoViewMatrixUniform = stateset->getUniform("stereoViewMatrices"); + auto* stereoViewProjectionsUniform = stateset->getUniform("stereoViewProjections"); + + stereoViewMatrixUniform->setElement(0, frustumViewMatrixInverse * mLeftCamera->getViewMatrix()); + stereoViewMatrixUniform->setElement(1, frustumViewMatrixInverse * mRightCamera->getViewMatrix()); + stereoViewProjectionsUniform->setElement(0, frustumViewMatrixInverse * mLeftCamera->getViewMatrix() * mLeftCamera->getProjectionMatrix()); + stereoViewProjectionsUniform->setElement(1, frustumViewMatrixInverse * mRightCamera->getViewMatrix() * mRightCamera->getProjectionMatrix()); + } + + void StereoView::setUpdateViewCallback(std::shared_ptr cb) + { + mUpdateViewCallback = cb; + } + + void disableStereoForCamera(osg::Camera* camera) + { + auto* viewport = camera->getViewport(); + camera->getOrCreateStateSet()->setAttribute(new osg::ViewportIndexed(0, viewport->x(), viewport->y(), viewport->width(), viewport->height()), osg::StateAttribute::OVERRIDE); + camera->getOrCreateStateSet()->addUniform(new osg::Uniform("geometryPassthrough", true), osg::StateAttribute::OVERRIDE); + } + + void enableStereoForCamera(osg::Camera* camera, bool horizontalSplit) + { + auto* viewport = camera->getViewport(); + auto x1 = viewport->x(); + auto y1 = viewport->y(); + auto width = viewport->width(); + auto height = viewport->height(); + + auto x2 = x1; + auto y2 = y1; + + if (horizontalSplit) + { + width /= 2; + x2 += width; + } + else + { + height /= 2; + y2 += height; + } + + camera->getOrCreateStateSet()->setAttribute(new osg::ViewportIndexed(0, x1, y1, width, height)); + camera->getOrCreateStateSet()->setAttribute(new osg::ViewportIndexed(1, x2, y2, width, height)); + camera->getOrCreateStateSet()->addUniform(new osg::Uniform("geometryPassthrough", false)); + } + + StereoView::Technique getStereoTechnique(void) + { + auto stereoMethodString = Settings::Manager::getString("stereo method", "Stereo"); + auto stereoMethodStringLowerCase = Misc::StringUtils::lowerCase(stereoMethodString); + if (stereoMethodStringLowerCase == "geometryshader") + { + return Misc::StereoView::Technique::GeometryShader_IndexedViewports; + } + if (stereoMethodStringLowerCase == "bruteforce") + { + return Misc::StereoView::Technique::BruteForce; + } + Log(Debug::Warning) << "Unknown stereo technique \"" << stereoMethodString << "\", defaulting to BruteForce"; + return StereoView::Technique::BruteForce; + } + + void StereoView::DefaultUpdateViewCallback::updateView(View& left, View& right) + { + left.pose.position = osg::Vec3(-2.2, 0, 0); + right.pose.position = osg::Vec3(2.2, 0, 0); + left.fov = { -0.767549932, 0.620896876, 0.726982594, -0.837898076 }; + right.fov = { -0.620896876, 0.767549932, 0.726982594, -0.837898076 }; + } + + void StereoView::setCullCallback(osg::ref_ptr cb) + { + mMainCamera->setCullCallback(cb); + } + + void StereoView::setCullMask(osg::Node::NodeMask cullMask) + { + mCullMask = cullMask; + if (mTechnique == Technique::GeometryShader_IndexedViewports) + { + mMainCamera->setCullMask(cullMask & ~mNoShaderMask); + mLeftCamera->setCullMask((cullMask & mNoShaderMask) | mSceneMask); + mRightCamera->setCullMask((cullMask & mNoShaderMask) | mSceneMask); + } + else + { + mMainCamera->setCullMask(cullMask); + mMainCamera->setCullMaskLeft(cullMask); + mMainCamera->setCullMaskRight(cullMask); + } + } + osg::Node::NodeMask StereoView::getCullMask() + { + return mCullMask; + } + osg::Matrixd StereoView::computeLeftEyeProjection(const osg::Matrixd& projection) const + { + return mLeftCamera->getProjectionMatrix(); + } + osg::Matrixd StereoView::computeLeftEyeView(const osg::Matrixd& view) const + { + return mLeftCamera->getViewMatrix(); + } + osg::Matrixd StereoView::computeRightEyeProjection(const osg::Matrixd& projection) const + { + return mRightCamera->getProjectionMatrix(); + } + osg::Matrixd StereoView::computeRightEyeView(const osg::Matrixd& view) const + { + return mRightCamera->getViewMatrix(); + } + void StereoView::StereoDrawCallback::operator()(osg::RenderInfo& info) const + { + // OSG does not give any information about stereo in these callbacks so i have to infer this myself. + // And hopefully OSG won't change this behaviour. + + View view = View::Both; + + auto camera = info.getCurrentCamera(); + auto viewport = camera->getViewport(); + + // Find the current scene view. + osg::GraphicsOperation* graphicsOperation = info.getCurrentCamera()->getRenderer(); + osgViewer::Renderer* renderer = dynamic_cast(graphicsOperation); + for (int i = 0; i < 2; i++) // OSG alternates between two sceneviews. + { + auto* sceneView = renderer->getSceneView(i); + // The render info argument is a member of scene view, allowing me to identify it. + if (&sceneView->getRenderInfo() == &info) + { + // Now i can simply examine the viewport. + auto activeViewport = static_cast(sceneView->getLocalStateSet()->getAttribute(osg::StateAttribute::Type::VIEWPORT, 0)); + if (activeViewport) + { + if (activeViewport->width() == viewport->width() && activeViewport->height() == viewport->height()) + view = View::Both; + else if (activeViewport->x() == viewport->x() && activeViewport->y() == viewport->y()) + view = View::Left; + else + view = View::Right; + } + else + { + // OSG always sets a viewport in the local stateset if osg's stereo is enabled. + // If it isn't, assume both. + view = View::Both; + } + + break; + } + } + + operator()(info, view); + } +} diff --git a/components/misc/stereo.hpp b/components/misc/stereo.hpp new file mode 100644 index 000000000..92b64713a --- /dev/null +++ b/components/misc/stereo.hpp @@ -0,0 +1,206 @@ +#ifndef MISC_STEREO_H +#define MISC_STEREO_H + +#include +#include +#include +#include + +#include + +#include + +// Some cursed headers like to define these +#if defined(near) || defined(far) +#undef near +#undef far +#endif + +namespace osgViewer +{ + class Viewer; +} + +namespace Misc +{ + //! Represents the relative pose in space of some object + 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); + + //! 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; + + osg::Matrix viewMatrix(bool useGLConventions); + }; + + //! Fov that defines all 4 angles from center + struct FieldOfView { + float angleLeft{ 0.f }; + float angleRight{ 0.f }; + float angleUp{ 0.f }; + float angleDown{ 0.f }; + + bool operator==(const FieldOfView& rhs) const; + + //! Generate a perspective matrix from this fov + osg::Matrix perspectiveMatrix(float near, float far) const; + }; + + //! Represents an eye including both pose and fov. + struct View + { + Pose pose; + FieldOfView fov; + bool operator==(const View& rhs) const; + }; + + //! Represent two eyes. The eyes are in relative terms, and are assumed to lie on the horizon plane. + struct StereoView + { + struct UpdateViewCallback + { + //! Called during the update traversal of every frame to source updated stereo values. + virtual void updateView(View& left, View& right) = 0; + }; + + //! Default implementation of UpdateViewCallback that just provides some hardcoded values for debugging purposes + struct DefaultUpdateViewCallback : public UpdateViewCallback + { + void updateView(View& left, View& right) override; + }; + + enum class Technique + { + None = 0, //!< Stereo disabled (do nothing). + BruteForce, //!< Two slave cameras culling and drawing everything. + GeometryShader_IndexedViewports, //!< Frustum camera culls and draws stereo into indexed viewports using an automatically generated geometry shader. + }; + + //! A draw callback that adds stereo information to the operator. + //! The stereo information is an enum describing which of the views the callback concerns. + //! With some stereo methods, there is only one callback, in which case the enum will be 'Both'. + //! + //! A typical use case of this callback is to prevent firing callbacks twice and correctly identifying the last/first callback. + struct StereoDrawCallback : public osg::Camera::DrawCallback + { + public: + enum class View + { + Both, Left, Right + }; + public: + StereoDrawCallback() + {} + + void operator()(osg::RenderInfo& info) const override; + + virtual void operator()(osg::RenderInfo& info, View view) const = 0; + + private: + }; + + static StereoView& instance(); + + //! Adds two cameras in stereo to the mainCamera. + //! All nodes matching the mask are rendered in stereo using brute force via two camera transforms, the rest are rendered in stereo via a geometry shader. + //! \param noShaderMask mask in all nodes that do not use shaders and must be rendered brute force. + //! \param sceneMask must equal MWRender::VisMask::Mask_Scene. Necessary while VisMask is still not in components/ + //! \note the masks apply only to the GeometryShader_IndexdViewports technique and can be 0 for the BruteForce technique. + StereoView(osg::Node::NodeMask noShaderMask, osg::Node::NodeMask sceneMask); + + //! Updates uniforms with the view and projection matrices of each stereo view, and replaces the camera's view and projection matrix + //! with a view and projection that closely envelopes the frustums of the two eyes. + void update(); + void updateStateset(osg::StateSet* stateset); + + void initializeStereo(osgViewer::Viewer* viewer, Technique technique); + //! Initialized scene. Call when the "scene root" node has been created + void initializeScene(); + + void setStereoTechnique(Technique technique); + + //! Callback that updates stereo configuration during the update pass + void setUpdateViewCallback(std::shared_ptr cb); + + //! Set the cull callback on the appropriate camera object + void setCullCallback(osg::ref_ptr cb); + + //! Apply the cullmask to the appropriate camera objects + void setCullMask(osg::Node::NodeMask cullMask); + + //! Get the last applied cullmask. + osg::Node::NodeMask getCullMask(); + + + osg::Matrixd computeLeftEyeProjection(const osg::Matrixd& projection) const; + osg::Matrixd computeLeftEyeView(const osg::Matrixd& view) const; + + osg::Matrixd computeRightEyeProjection(const osg::Matrixd& projection) const; + osg::Matrixd computeRightEyeView(const osg::Matrixd& view) const; + + private: + void setupBruteForceTechnique(); + void setupGeometryShaderIndexedViewportTechnique(); + void removeBruteForceTechnique(); + void removeGeometryShaderIndexedViewportTechnique(); + void disableStereo(); + void enableStereo(); + + osg::ref_ptr mViewer; + osg::ref_ptr mMainCamera; + osg::ref_ptr mRoot; + osg::ref_ptr mScene; + osg::ref_ptr mStereoRoot; + osg::ref_ptr mUpdateCallback; + Technique mTechnique; + + // Keeps state relevant to doing stereo via the geometry shader + osg::ref_ptr mStereoGeometryShaderRoot{ new osg::Group }; + osg::Node::NodeMask mNoShaderMask; + osg::Node::NodeMask mSceneMask; + osg::Node::NodeMask mCullMask; + + // Keeps state and cameras relevant to doing stereo via brute force + osg::ref_ptr mStereoBruteForceRoot{ new osg::Group }; + osg::ref_ptr mLeftCamera{ new osg::Camera }; + osg::ref_ptr mRightCamera{ new osg::Camera }; + + using SharedShadowMapConfig = SceneUtil::MWShadowTechnique::SharedShadowMapConfig; + osg::ref_ptr mMasterConfig; + osg::ref_ptr mSlaveConfig; + bool mSharedShadowMaps; + + // Camera viewports + bool flipViewOrder{ true }; + + // Updates stereo configuration during the update pass + std::shared_ptr mUpdateViewCallback; + + // OSG camera callbacks set using set*callback. StereoView manages that these are always set on the appropriate camera(s); + osg::ref_ptr mCullCallback{ nullptr }; + }; + + //! Overrides all stereo-related states/uniforms to disable stereo for the scene rendered by camera + void disableStereoForCamera(osg::Camera* camera); + + //! Overrides all stereo-related states/uniforms to enable stereo for the scene rendered by camera + void enableStereoForCamera(osg::Camera* camera, bool horizontalSplit); + + //! Reads settings to determine stereo technique + StereoView::Technique getStereoTechnique(void); +} + +#endif diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 48deaa999..665ee003e 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -164,6 +164,19 @@ public: return out; } + /// true if the string is a number + static bool isNumber(const std::string& in) + { + for (auto c : in) + { + if (!std::isdigit(c)) + { + return false; + } + } + return true; + } + struct CiComp { bool operator()(const std::string& left, const std::string& right) const diff --git a/components/myguiplatform/additivelayer.cpp b/components/myguiplatform/additivelayer.cpp index 49558cf8e..e25ef8a72 100644 --- a/components/myguiplatform/additivelayer.cpp +++ b/components/myguiplatform/additivelayer.cpp @@ -21,13 +21,13 @@ namespace osgMyGUI void AdditiveLayer::renderToTarget(MyGUI::IRenderTarget *_target, bool _update) { - RenderManager& renderManager = static_cast(MyGUI::RenderManager::getInstance()); + StateInjectableRenderTarget* injectableTarget = static_cast(_target); - renderManager.setInjectState(mStateSet.get()); + injectableTarget->setInjectState(mStateSet.get()); MyGUI::OverlappedLayer::renderToTarget(_target, _update); - renderManager.setInjectState(nullptr); + injectableTarget->setInjectState(nullptr); } } diff --git a/components/myguiplatform/additivelayer.hpp b/components/myguiplatform/additivelayer.hpp index f89e1d007..eb8a2a719 100644 --- a/components/myguiplatform/additivelayer.hpp +++ b/components/myguiplatform/additivelayer.hpp @@ -14,7 +14,7 @@ namespace osgMyGUI { /// @brief A Layer rendering with additive blend mode. - class AdditiveLayer final : public MyGUI::OverlappedLayer + class AdditiveLayer : public MyGUI::OverlappedLayer { public: MYGUI_RTTI_DERIVED( AdditiveLayer ) diff --git a/components/myguiplatform/myguiplatform.hpp b/components/myguiplatform/myguiplatform.hpp index 5ffbe0be7..b9bc86fb3 100644 --- a/components/myguiplatform/myguiplatform.hpp +++ b/components/myguiplatform/myguiplatform.hpp @@ -47,6 +47,7 @@ namespace osgMyGUI DataManager* mDataManager; MyGUI::LogManager* mLogManager; LogFacility* mLogFacility; + bool mVRMode; void operator=(const Platform&); Platform(const Platform&); diff --git a/components/myguiplatform/myguirendermanager.cpp b/components/myguiplatform/myguirendermanager.cpp index 77a5ee533..c59d02c08 100644 --- a/components/myguiplatform/myguirendermanager.cpp +++ b/components/myguiplatform/myguirendermanager.cpp @@ -1,7 +1,12 @@ #include "myguirendermanager.hpp" +#include + #include #include +#include +#include +#include #include #include @@ -43,8 +48,10 @@ namespace osgMyGUI { +class GUICamera; + class Drawable : public osg::Drawable { - osgMyGUI::RenderManager *mParent; + osgMyGUI::RenderManager *mManager; osg::ref_ptr mStateSet; public: @@ -78,26 +85,26 @@ public: { public: CollectDrawCalls() - : mRenderManager(nullptr) + : mCamera(nullptr) + , mFilter("") { } - void setRenderManager(osgMyGUI::RenderManager* renderManager) + void setCamera(osgMyGUI::GUICamera* camera) { - mRenderManager = renderManager; + mCamera = camera; } - bool cull(osg::NodeVisitor*, osg::Drawable*, osg::State*) const override + void setFilter(std::string filter) { - if (!mRenderManager) - return false; - - mRenderManager->collectDrawCalls(); - return false; + mFilter = filter; } + bool cull(osg::NodeVisitor*, osg::Drawable*, osg::State*) const override; + private: - osgMyGUI::RenderManager* mRenderManager; + GUICamera* mCamera; + std::string mFilter; }; // Stage 2: execute the draw calls. Run during the Draw traversal. May run in parallel with the update traversal of the next frame. @@ -168,20 +175,24 @@ public: } public: - Drawable(osgMyGUI::RenderManager *parent = nullptr) - : mParent(parent) + Drawable(std::string filter = "", osgMyGUI::RenderManager *manager = nullptr, osgMyGUI::GUICamera* camera = nullptr) + : mManager(manager) , mWriteTo(0) , mReadFrom(0) { setSupportsDisplayList(false); osg::ref_ptr collectDrawCalls = new CollectDrawCalls; - collectDrawCalls->setRenderManager(mParent); + collectDrawCalls->setCamera(camera); + collectDrawCalls->setFilter(filter); setCullCallback(collectDrawCalls); - osg::ref_ptr frameUpdate = new FrameUpdate; - frameUpdate->setRenderManager(mParent); - setUpdateCallback(frameUpdate); + if (mManager) + { + osg::ref_ptr frameUpdate = new FrameUpdate; + frameUpdate->setRenderManager(mManager); + setUpdateCallback(frameUpdate); + } mStateSet = new osg::StateSet; mStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF); @@ -197,7 +208,7 @@ public: } Drawable(const Drawable ©, const osg::CopyOp ©op=osg::CopyOp::SHALLOW_COPY) : osg::Drawable(copy, copyop) - , mParent(copy.mParent) + , mManager(copy.mManager) , mStateSet(copy.mStateSet) , mWriteTo(0) , mReadFrom(0) @@ -350,26 +361,101 @@ osg::VertexBufferObject* OSGVertexBuffer::getVertexBuffer() // --------------------------------------------------------------------------- +/// Camera used to draw a MyGUI layer +class GUICamera : public osg::Camera, public StateInjectableRenderTarget +{ +public: + GUICamera(osg::Camera::RenderOrder order, RenderManager* parent, std::string filter) + : mParent(parent) + , mUpdate(false) + , mFilter(filter) + { + setReferenceFrame(osg::Transform::ABSOLUTE_RF); + setProjectionResizePolicy(osg::Camera::FIXED); + setProjectionMatrix(osg::Matrix::identity()); + setViewMatrix(osg::Matrix::identity()); + setRenderOrder(order); + setClearMask(GL_NONE); + setName("GUI Camera"); + mDrawable = new Drawable(filter, parent, this); + mDrawable->setName("GUI Drawable"); + mDrawable->setDataVariance(osg::Object::STATIC); + addChild(mDrawable.get()); + mDrawable->setCullingActive(false); + } + + ~GUICamera() + { + mParent->deleteGUICamera(this); + } + + + // Called by the cull traversal + /** @see IRenderTarget::begin */ + void begin() override; + void end() override; + + /** @see IRenderTarget::doRender */ + void doRender(MyGUI::IVertexBuffer* buffer, MyGUI::ITexture* texture, size_t count) override; + + + void collectDrawCalls(); + void collectDrawCalls(std::string filter); + + void setViewSize(MyGUI::IntSize viewSize); + + /** @see IRenderTarget::getInfo */ + const MyGUI::RenderTargetInfo& getInfo() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mInfo; } + + RenderManager* mParent; + osg::ref_ptr mDrawable; + MyGUI::RenderTargetInfo mInfo; + bool mUpdate; + std::string mFilter; +}; + + +void GUICamera::begin() +{ + mDrawable->clear(); + // variance will be recomputed based on textures being rendered in this frame + mDrawable->setDataVariance(osg::Object::STATIC); +} + +bool Drawable::CollectDrawCalls::cull(osg::NodeVisitor*, osg::Drawable*, osg::State*) const +{ + if (!mCamera) + return false; + + if (mFilter.empty()) + mCamera->collectDrawCalls(); + else + mCamera->collectDrawCalls(mFilter); + return false; +} + RenderManager::RenderManager(osgViewer::Viewer *viewer, osg::Group *sceneroot, Resource::ImageManager* imageManager, float scalingFactor) : mViewer(viewer) , mSceneRoot(sceneroot) , mImageManager(imageManager) - , mUpdate(false) , mIsInitialise(false) , mInvScalingFactor(1.f) - , mInjectState(nullptr) { if (scalingFactor != 0.f) mInvScalingFactor = 1.f / scalingFactor; + + + osg::ref_ptr vp = mViewer->getCamera()->getViewport(); + setViewSize(vp->width(), vp->height()); } RenderManager::~RenderManager() { MYGUI_PLATFORM_LOG(Info, "* Shutdown: "<removeChild(mGuiRoot.get()); - mGuiRoot = nullptr; + for (auto guiCamera : mGuiCameras) + mSceneRoot->removeChild(guiCamera); + mGuiCameras.clear(); mSceneRoot = nullptr; mViewer = nullptr; @@ -387,25 +473,7 @@ void RenderManager::initialise() mVertexFormat = MyGUI::VertexColourType::ColourABGR; - mUpdate = false; - - mDrawable = new Drawable(this); - - osg::ref_ptr camera = new osg::Camera(); - camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); - camera->setProjectionResizePolicy(osg::Camera::FIXED); - camera->setProjectionMatrix(osg::Matrix::identity()); - camera->setViewMatrix(osg::Matrix::identity()); - camera->setRenderOrder(osg::Camera::POST_RENDER); - camera->setClearMask(GL_NONE); - mDrawable->setCullingActive(false); - camera->addChild(mDrawable.get()); - - mGuiRoot = camera; - mSceneRoot->addChild(mGuiRoot.get()); - - osg::ref_ptr vp = mViewer->getCamera()->getViewport(); - setViewSize(vp->width(), vp->height()); + mSceneRoot->addChild(createGUICamera(osg::Camera::POST_RENDER, "")); MYGUI_PLATFORM_LOG(Info, getClassTypeName()<<" successfully initialized"); mIsInitialise = true; @@ -413,8 +481,12 @@ void RenderManager::initialise() void RenderManager::shutdown() { - mGuiRoot->removeChildren(0, mGuiRoot->getNumChildren()); - mSceneRoot->removeChild(mGuiRoot); + // TODO: Is this method meaningful? Why not just let the destructor handle everything? + for (auto guiCamera : mGuiCameras) + { + guiCamera->removeChildren(0, guiCamera->getNumChildren()); + mSceneRoot->removeChild(guiCamera); + } } MyGUI::IVertexBuffer* RenderManager::createVertexBuffer() @@ -427,15 +499,7 @@ void RenderManager::destroyVertexBuffer(MyGUI::IVertexBuffer *buffer) delete buffer; } - -void RenderManager::begin() -{ - mDrawable->clear(); - // variance will be recomputed based on textures being rendered in this frame - mDrawable->setDataVariance(osg::Object::STATIC); -} - -void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *texture, size_t count) +void GUICamera::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *texture, size_t count) { Drawable::Batch batch; batch.mVertexCount = count; @@ -450,6 +514,7 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text mDrawable->setDataVariance(osg::Object::DYNAMIC); // only for this frame, reset in begin() batch.mTexture->getUserValue("premultiplied alpha", premultipliedAlpha); } + if (mInjectState) batch.mStateSet = mInjectState; else if (premultipliedAlpha) @@ -463,12 +528,12 @@ void RenderManager::doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *text mDrawable->addBatch(batch); } -void RenderManager::setInjectState(osg::StateSet* stateSet) +void StateInjectableRenderTarget::setInjectState(osg::StateSet* stateSet) { mInjectState = stateSet; } -void RenderManager::end() +void GUICamera::end() { } @@ -484,33 +549,84 @@ void RenderManager::update() last_time = now_time; } -void RenderManager::collectDrawCalls() +void GUICamera::collectDrawCalls() { begin(); - onRenderToTarget(this, mUpdate); + MyGUI::LayerManager* myGUILayers = MyGUI::LayerManager::getInstancePtr(); + if (myGUILayers != nullptr) + { + for (unsigned i = 0; i < myGUILayers->getLayerCount(); i++) + { + auto layer = myGUILayers->getLayer(i); + layer->renderToTarget(this, mUpdate); + } + } end(); mUpdate = false; } -void RenderManager::setViewSize(int width, int height) +void GUICamera::collectDrawCalls(std::string filter) { - if(width < 1) width = 1; - if(height < 1) height = 1; + begin(); + MyGUI::LayerManager* myGUILayers = MyGUI::LayerManager::getInstancePtr(); + if (myGUILayers != nullptr) + { + for (unsigned i = 0; i < myGUILayers->getLayerCount(); i++) + { + auto layer = myGUILayers->getLayer(i); + auto name = layer->getName(); - mGuiRoot->setViewport(0, 0, width, height); + if (filter.find(name) != std::string::npos) + { + layer->renderToTarget(this, mUpdate); + } + } + } + end(); - mViewSize.set(width * mInvScalingFactor, height * mInvScalingFactor); + mUpdate = false; +} +void GUICamera::setViewSize(MyGUI::IntSize viewSize) +{ mInfo.maximumDepth = 1; mInfo.hOffset = 0; mInfo.vOffset = 0; - mInfo.aspectCoef = float(mViewSize.height) / float(mViewSize.width); - mInfo.pixScaleX = 1.0f / float(mViewSize.width); - mInfo.pixScaleY = 1.0f / float(mViewSize.height); + mInfo.aspectCoef = float(viewSize.height) / float(viewSize.width); + mInfo.pixScaleX = 1.0f / float(viewSize.width); + mInfo.pixScaleY = 1.0f / float(viewSize.height); + mUpdate = true; +} + +void RenderManager::setViewSize(int width, int height) +{ + if(width < 1) width = 1; + if(height < 1) height = 1; + + mViewSize.set(width * mInvScalingFactor, height * mInvScalingFactor); + for (auto* camera : mGuiCameras) + { + GUICamera* guiCamera = static_cast(camera); + guiCamera->setViewport(0, 0, width, height); + guiCamera->setViewSize(mViewSize); + } onResizeView(mViewSize); - mUpdate = true; +} + +osg::ref_ptr RenderManager::createGUICamera(int order, std::string layerFilter) +{ + osg::ref_ptr camera = new GUICamera(static_cast(order), this, layerFilter); + mGuiCameras.insert(camera); + camera->setViewport(0, 0, mViewSize.width, mViewSize.height); + camera->setViewSize(mViewSize); + return camera; +} + +void RenderManager::deleteGUICamera(GUICamera* camera) +{ + mGuiCameras.erase(camera); } diff --git a/components/myguiplatform/myguirendermanager.hpp b/components/myguiplatform/myguirendermanager.hpp index 3c3fb672d..9b46e2b82 100644 --- a/components/myguiplatform/myguirendermanager.hpp +++ b/components/myguiplatform/myguirendermanager.hpp @@ -4,6 +4,7 @@ #include #include +#include #include "myguicompat.h" @@ -29,29 +30,43 @@ namespace osgMyGUI { class Drawable; +class GUICamera; -class RenderManager : public MyGUI::RenderManager, public MyGUI::IRenderTarget +class StateInjectableRenderTarget : public MyGUI::IRenderTarget +{ +public: + StateInjectableRenderTarget() = default; + ~StateInjectableRenderTarget() = default; + + /** specify a StateSet to inject for rendering. The StateSet will be used by future doRender calls until you reset it to nullptr again. */ + void setInjectState(osg::StateSet* stateSet); + +protected: + osg::StateSet* mInjectState{ nullptr }; +}; + +class RenderManager : public MyGUI::RenderManager { osg::ref_ptr mViewer; osg::ref_ptr mSceneRoot; - osg::ref_ptr mDrawable; + osg::ref_ptr mGuiCamera; + std::set mGuiCameras; Resource::ImageManager* mImageManager; - MyGUI::IntSize mViewSize; - bool mUpdate; + MyGUI::VertexColourType mVertexFormat; MyGUI::RenderTargetInfo mInfo; + typedef std::map MapTexture; MapTexture mTextures; bool mIsInitialise; - osg::ref_ptr mGuiRoot; - float mInvScalingFactor; - osg::StateSet* mInjectState; + + bool mVRMode; void destroyAllResources(); @@ -93,20 +108,6 @@ public: // Called by the update traversal void update(); - // Called by the cull traversal - /** @see IRenderTarget::begin */ - void begin() override; - /** @see IRenderTarget::end */ - void end() override; - /** @see IRenderTarget::doRender */ - void doRender(MyGUI::IVertexBuffer *buffer, MyGUI::ITexture *texture, size_t count) override; - - /** specify a StateSet to inject for rendering. The StateSet will be used by future doRender calls until you reset it to nullptr again. */ - void setInjectState(osg::StateSet* stateSet); - - /** @see IRenderTarget::getInfo */ - const MyGUI::RenderTargetInfo& getInfo() OPENMW_MYGUI_CONST_GETTER_3_4_1 override { return mInfo; } - bool checkTexture(MyGUI::ITexture* _texture); // setViewSize() is a part of MyGUI::RenderManager interface since 3.4.0 release @@ -124,6 +125,8 @@ public: /*internal:*/ void collectDrawCalls(); + osg::ref_ptr createGUICamera(int order, std::string layerFilter); + void deleteGUICamera(GUICamera* camera); }; } diff --git a/components/myguiplatform/scalinglayer.hpp b/components/myguiplatform/scalinglayer.hpp index f9fd92a78..70f476638 100644 --- a/components/myguiplatform/scalinglayer.hpp +++ b/components/myguiplatform/scalinglayer.hpp @@ -8,7 +8,7 @@ namespace osgMyGUI ///@brief A Layer that lays out and renders widgets in screen-relative coordinates. The "Size" property determines the size of the virtual screen, /// which is then upscaled to the real screen size during rendering. The aspect ratio is kept intact, adding blanks to the sides when necessary. - class ScalingLayer final : public MyGUI::OverlappedLayer + class ScalingLayer : public MyGUI::OverlappedLayer { public: MYGUI_RTTI_DERIVED(ScalingLayer) diff --git a/components/sceneutil/lightmanager.cpp b/components/sceneutil/lightmanager.cpp index 63a475566..85d8c1638 100644 --- a/components/sceneutil/lightmanager.cpp +++ b/components/sceneutil/lightmanager.cpp @@ -1335,7 +1335,10 @@ namespace SceneUtil transformBoundingSphere(mat, nodeBound); mLightList.clear(); - for (size_t i = 0; i < lights.size(); ++i) + for (size_t i=0; i maxLights) + if (mLightListCropped.empty()) { - // remove lights culled by this camera - LightManager::LightList lightList = mLightList; - for (auto it = lightList.begin(); it != lightList.end() && lightList.size() > maxLights;) + mLightListCropped = mLightList; + if (mLightList.size() > maxLights) { - osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); - - osg::BoundingSphere bs = (*it)->mViewBound; - bs._radius = bs._radius * 2.0; - osg::CullingSet& cullingSet = stack.front(); - if (cullingSet.isCulled(bs)) + // remove lights culled by this camera + for (auto it = mLightListCropped.begin(); it != mLightListCropped.end() && mLightListCropped.size() > maxLights; ) { - it = lightList.erase(it); - continue; + osg::CullStack::CullingStack& stack = cv->getModelViewCullingStack(); + + osg::BoundingSphere bs = (*it)->mViewBound; + bs._radius = bs._radius * 2.0; + osg::CullingSet& cullingSet = stack.front(); + if (cullingSet.isCulled(bs)) + { + it = mLightListCropped.erase(it); + continue; + } + else + ++it; } - else - ++it; - } - if (lightList.size() > maxLights) - { - // sort by proximity to camera, then get rid of furthest away lights - std::sort(lightList.begin(), lightList.end(), sortLights); - while (lightList.size() > maxLights) - lightList.pop_back(); + if (mLightListCropped.size() > maxLights) + { + // sort by proximity to camera, then get rid of furthest away lights + std::sort(mLightListCropped.begin(), mLightListCropped.end(), sortLights); + while (mLightListCropped.size() > maxLights) + mLightListCropped.pop_back(); + } } - stateset = mLightManager->getLightListStateSet(lightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); } - else - stateset = mLightManager->getLightListStateSet(mLightList, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); + stateset = mLightManager->getLightListStateSet(mLightListCropped, cv->getTraversalNumber(), cv->getCurrentRenderStage()->getInitialViewMatrix()); cv->pushStateSet(stateset); return true; diff --git a/components/sceneutil/lightmanager.hpp b/components/sceneutil/lightmanager.hpp index 6dbe7a3f7..d9a0dd2e7 100644 --- a/components/sceneutil/lightmanager.hpp +++ b/components/sceneutil/lightmanager.hpp @@ -280,6 +280,7 @@ namespace SceneUtil LightManager* mLightManager; size_t mLastFrameNumber; LightManager::LightList mLightList; + LightManager::LightList mLightListCropped; std::set mIgnoredLightSources; }; diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index 745d93daf..efc870885 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -18,6 +18,8 @@ #include "mwshadowtechnique.hpp" +#include + #include #include #include @@ -588,6 +590,9 @@ MWShadowTechnique::ShadowData::ShadowData(MWShadowTechnique::ViewDependentData* // set viewport _camera->setViewport(0,0,textureSize.x(),textureSize.y()); + // Shadow casting should not obey indexed viewports + Misc::disableStereoForCamera(_camera); + if (debug) { @@ -926,130 +931,138 @@ MWShadowTechnique::ViewDependentData* MWShadowTechnique::getViewDependentData(os return vdd.release(); } -void MWShadowTechnique::update(osg::NodeVisitor& nv) +MWShadowTechnique::ViewDependentData* MWShadowTechnique::getSharedVdd(const SharedShadowMapConfig& config) { - OSG_INFO<<"MWShadowTechnique::update(osg::NodeVisitor& "<<&nv<<")"<osg::Group::traverse(nv); + auto it = _viewDependentDataShareMap.find(config._id); + if (it != _viewDependentDataShareMap.end()) + return it->second; + + return nullptr; } -void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) +void MWShadowTechnique::addSharedVdd(const SharedShadowMapConfig& config, ViewDependentData* vdd) { - if (!_enableShadows) - { - if (mSetDummyStateWhenDisabled) - { - osg::ref_ptr dummyState = new osg::StateSet(); + _viewDependentDataShareMap[config._id] = vdd; +} - ShadowSettings* settings = getShadowedScene()->getShadowSettings(); - int baseUnit = settings->getBaseShadowTextureUnit(); - int endUnit = baseUnit + settings->getNumShadowMapsPerLight(); - for (int i = baseUnit; i < endUnit; ++i) - { - dummyState->setTextureAttributeAndModes(i, _fallbackShadowMapTexture, osg::StateAttribute::ON); - dummyState->addUniform(new osg::Uniform(("shadowTexture" + std::to_string(i - baseUnit)).c_str(), i)); - dummyState->addUniform(new osg::Uniform(("shadowTextureUnit" + std::to_string(i - baseUnit)).c_str(), i)); - } +void SceneUtil::MWShadowTechnique::shareShadowMap(osgUtil::CullVisitor& cv, ViewDependentData* lhs, ViewDependentData* rhs) +{ + // Prepare for rendering shadows using the shadow map owned by rhs. - cv.pushStateSet(dummyState); - } + // To achieve this i first copy all data that is not specific to this cv's camera and thus read-only, + // trusting openmw and osg won't overwrite that data before this frame is done rendering. + // This works due to the double buffering of CullVisitors by osg, but also requires that cull passes are serialized (relative to one another). + // Then initialize new copies of the data that will be written with view-specific data + // (the stateset and the texgens). - _shadowedScene->osg::Group::traverse(cv); + lhs->_viewDependentShadowMap = rhs->_viewDependentShadowMap; + auto* stateset = lhs->getStateSet(cv.getTraversalNumber()); + stateset->clear(); + lhs->_lightDataList = rhs->_lightDataList; + lhs->_numValidShadows = rhs->_numValidShadows; - if (mSetDummyStateWhenDisabled) - cv.popStateSet(); + ShadowDataList& sdl = lhs->getShadowDataList(); + ShadowDataList previous_sdl; + previous_sdl.swap(sdl); + for (auto rhs_sd : rhs->getShadowDataList()) + { + osg::ref_ptr lhs_sd; - return; + if (previous_sdl.empty()) + { + OSG_INFO << "Create new ShadowData" << std::endl; + lhs_sd = new ShadowData(lhs); + } + else + { + OSG_INFO << "Taking ShadowData from from of previous_sdl" << std::endl; + lhs_sd = previous_sdl.front(); + previous_sdl.erase(previous_sdl.begin()); + } + lhs_sd->_camera = rhs_sd->_camera; + lhs_sd->_textureUnit = rhs_sd->_textureUnit; + lhs_sd->_texture = rhs_sd->_texture; + sdl.push_back(lhs_sd); } +} - OSG_INFO<(cv.getUserData()); + if (!sharedConfig) + sharedConfig = dynamic_cast(cv.getCurrentCamera()->getUserData()); + if (!sharedConfig) { - OSG_INFO<<"Warning, init() has not yet been called so ShadowCastingStateSet has not been setup yet, unable to create shadows."<osg::Group::traverse(cv); - return; + return false; } - ViewDependentData* vdd = getViewDependentData(&cv); - - if (!vdd) + if (sharedConfig->_master) { - OSG_INFO<<"Warning, now ViewDependentData created, unable to create shadows."<osg::Group::traverse(cv); - return; + addSharedVdd(*sharedConfig, vdd); + if(sharedConfig->_projection) + cv.pushProjectionMatrix(sharedConfig->_projection); + if(sharedConfig->_modelView) + cv.pushModelViewMatrix(sharedConfig->_modelView, sharedConfig->_referenceFrame); + return false; } - - ShadowSettings* settings = getShadowedScene()->getShadowSettings(); - - OSG_INFO<<"cv->getProjectionMatrix()="<<*cv.getProjectionMatrix()<_projection) + cv.popProjectionMatrix(); + if (sharedConfig->_modelView) + cv.popModelViewMatrix(); } +} - // clamp the minZNear and maxZFar to those provided by ShadowSettings - maxZFar = osg::minimum(settings->getMaximumShadowMapDistance(),maxZFar); - if (minZNear>maxZFar) minZNear = maxZFar*settings->getMinimumShadowMapNearFarRatio(); +void SceneUtil::MWShadowTechnique::castShadows(osgUtil::CullVisitor& cv, ViewDependentData* vdd) +{ + ShadowSettings* settings = getShadowedScene()->getShadowSettings(); + osg::RefMatrix& viewProjectionMatrix = *cv.getProjectionMatrix(); + // check whether this main views projection is perspective or orthographic + bool orthographicViewFrustum = viewProjectionMatrix(0,3)==0.0 && + viewProjectionMatrix(1,3)==0.0 && + viewProjectionMatrix(2,3)==0.0; + + // Compute near/far of the camera's projection matrix + double minZNear = 0.0; + double maxZFar = dbl_max; + computeProjectionNearFar(cv, orthographicViewFrustum, minZNear, maxZFar); //OSG_NOTICE<<"maxZFar "< vertexArray = new osg::Vec3Array(); - for (osg::Vec3d &vertex : frustum.corners) - vertexArray->push_back((osg::Vec3)vertex); - _debugHud->setFrustumVertices(vertexArray, cv.getTraversalNumber()); - } - + // Reduce near/far as much as possible double reducedNear, reducedFar; if (cv.getComputeNearFarMode() != osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR) { @@ -1062,8 +1075,14 @@ void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) reducedFar = maxZFar; } - // return compute near far mode back to it's original settings - cv.setComputeNearFarMode(cachedNearFarMode); + Frustum frustum(&cv, minZNear, maxZFar); + if (_debugHud) + { + osg::ref_ptr vertexArray = new osg::Vec3Array(); + for (osg::Vec3d& vertex : frustum.corners) + vertexArray->push_back((osg::Vec3)vertex); + _debugHud->setFrustumVertices(vertexArray, cv.getTraversalNumber()); + } OSG_INFO<<"frustum.eye="<osg::Group::traverse(nv); +} + +void MWShadowTechnique::cull(osgUtil::CullVisitor& cv) +{ + + if (!_enableShadows) + { + if (mSetDummyStateWhenDisabled) + { + osg::ref_ptr dummyState = new osg::StateSet(); + + ShadowSettings* settings = getShadowedScene()->getShadowSettings(); + int baseUnit = settings->getBaseShadowTextureUnit(); + int endUnit = baseUnit + settings->getNumShadowMapsPerLight(); + for (int i = baseUnit; i < endUnit; ++i) + { + dummyState->setTextureAttributeAndModes(i, _fallbackShadowMapTexture, osg::StateAttribute::ON); + dummyState->addUniform(new osg::Uniform(("shadowTexture" + std::to_string(i - baseUnit)).c_str(), i)); + dummyState->addUniform(new osg::Uniform(("shadowTextureUnit" + std::to_string(i - baseUnit)).c_str(), i)); + } + + cv.pushStateSet(dummyState); + } + + _shadowedScene->osg::Group::traverse(cv); + + if (mSetDummyStateWhenDisabled) + cv.popStateSet(); + + return; + } + + OSG_INFO<osg::Group::traverse(cv); + return; + } + + ViewDependentData* vdd = getViewDependentData(&cv); + + if (!vdd) + { + OSG_INFO<<"Warning, now ViewDependentData created, unable to create shadows."<osg::Group::traverse(cv); + return; + } + + ShadowSettings* settings = getShadowedScene()->getShadowSettings(); + osg::CullSettings::ComputeNearFarMode cachedNearFarMode = cv.getComputeNearFarMode(); + + OSG_INFO<<"cv->getProjectionMatrix()="<<*cv.getProjectionMatrix()<getComputeNearFarModeOverride()!=osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR) + { + cv.setComputeNearFarMode(settings->getComputeNearFarModeOverride()); + } + + // 1. Traverse main scene graph + auto* shadowReceiverStateSet = vdd->getStateSet(cv.getTraversalNumber()); + shadowReceiverStateSet->clear(); + cv.pushStateSet(shadowReceiverStateSet); + + cullShadowReceivingScene(&cv); + + cv.popStateSet(); + + if (cv.getComputeNearFarMode()!=osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR) + { + OSG_INFO<<"Just done main subgraph traversak"<0) + if (vdd->_numValidShadows>0) { prepareStateSetForRenderingShadow(*vdd, cv.getTraversalNumber()); } @@ -2985,7 +3137,7 @@ bool MWShadowTechnique::assignTexGenSettings(osgUtil::CullVisitor* cv, osg::Came // Place texgen with modelview which removes big offsets (making it float friendly) osg::ref_ptr refMatrix = - new osg::RefMatrix( camera->getInverseViewMatrix() * (*(cv->getModelViewMatrix())) ); + new osg::RefMatrix( camera->getInverseViewMatrix() * (*cv->getModelViewMatrix()) ); osgUtil::RenderStage* currentStage = cv->getCurrentRenderBin()->getStage(); currentStage->getPositionalStateContainer()->addPositionedTextureAttribute( textureUnit, refMatrix.get(), texgen ); diff --git a/components/sceneutil/mwshadowtechnique.hpp b/components/sceneutil/mwshadowtechnique.hpp index de57bf1fd..3ce63d72f 100644 --- a/components/sceneutil/mwshadowtechnique.hpp +++ b/components/sceneutil/mwshadowtechnique.hpp @@ -142,6 +142,28 @@ namespace SceneUtil { // forward declare class ViewDependentData; + /// Configuration of shadow maps shared by multiple views + struct SharedShadowMapConfig : public osg::Referenced + { + virtual ~SharedShadowMapConfig() {} + + /// String identifier of the shared shadow map + std::string _id{ "" }; + + /// If true, this camera will generate the shadow map + bool _master{ false }; + + /// If set, will override projection matrix of camera when generating shadow map. + osg::ref_ptr _projection{ nullptr }; + + /// If set, will override model view matrix of camera when generating shadow map. + osg::ref_ptr _modelView{ nullptr }; + + /// Reference frame of the view matrix + osg::Transform::ReferenceFrame + _referenceFrame{ osg::Transform::ABSOLUTE_RF }; + }; + struct LightData : public osg::Referenced { LightData(ViewDependentData* vdd); @@ -196,7 +218,12 @@ namespace SceneUtil { virtual void releaseGLObjects(osg::State* = 0) const; + unsigned int numValidShadows(void) const { return _numValidShadows; } + + void setNumValidShadows(unsigned int numValidShadows) { _numValidShadows = numValidShadows; } + protected: + friend class MWShadowTechnique; virtual ~ViewDependentData() {} MWShadowTechnique* _viewDependentShadowMap; @@ -205,13 +232,29 @@ namespace SceneUtil { LightDataList _lightDataList; ShadowDataList _shadowDataList; + + unsigned int _numValidShadows; }; virtual ViewDependentData* createViewDependentData(osgUtil::CullVisitor* cv); ViewDependentData* getViewDependentData(osgUtil::CullVisitor* cv); + ViewDependentData* getSharedVdd(const SharedShadowMapConfig& config); + + void addSharedVdd(const SharedShadowMapConfig& config, ViewDependentData* vdd); + + void shareShadowMap(osgUtil::CullVisitor& cv, ViewDependentData* lhs, ViewDependentData* rhs); + + bool trySharedShadowMap(osgUtil::CullVisitor& cv, ViewDependentData* vdd); + + void endSharedShadowMap(osgUtil::CullVisitor& cv); + + void castShadows(osgUtil::CullVisitor& cv, ViewDependentData* vdd); + + void assignTexGenSettings(osgUtil::CullVisitor& cv, ViewDependentData* vdd); + void computeProjectionNearFar(osgUtil::CullVisitor& cv, bool orthographicViewFrustum, double& znear, double& zfar); virtual void createShaders(); @@ -240,7 +283,9 @@ namespace SceneUtil { typedef std::map< osgUtil::CullVisitor*, osg::ref_ptr > ViewDependentDataMap; mutable std::mutex _viewDependentDataMapMutex; + typedef std::map< std::string, osg::ref_ptr > ViewDependentDataShareMap; ViewDependentDataMap _viewDependentDataMap; + ViewDependentDataShareMap _viewDependentDataShareMap; osg::ref_ptr _shadowRecievingPlaceholderStateSet; diff --git a/components/sceneutil/riggeometry.cpp b/components/sceneutil/riggeometry.cpp index b9201fdf6..3e4968354 100644 --- a/components/sceneutil/riggeometry.cpp +++ b/components/sceneutil/riggeometry.cpp @@ -42,6 +42,7 @@ RigGeometry::RigGeometry() { setNumChildrenRequiringUpdateTraversal(1); // update done in accept(NodeVisitor&) + setCullingActive(false); } RigGeometry::RigGeometry(const RigGeometry ©, const osg::CopyOp ©op) @@ -195,6 +196,10 @@ void RigGeometry::cull(osg::NodeVisitor* nv) mSkeleton->updateBoneMatrices(traversalNumber); + // Tracking login in VR updates bone matrices out of order, and forces bounds to be recalculated during cull. + if (mSkeleton->isTracked()) + updateBounds(nv); + // skinning const osg::Vec3Array* positionSrc = static_cast(mSourceGeometry->getVertexArray()); const osg::Vec3Array* normalSrc = static_cast(mSourceGeometry->getNormalArray()); diff --git a/components/sceneutil/rtt.cpp b/components/sceneutil/rtt.cpp new file mode 100644 index 000000000..a661a9027 --- /dev/null +++ b/components/sceneutil/rtt.cpp @@ -0,0 +1,110 @@ +#include "rtt.hpp" +#include "util.hpp" + +#include +#include +#include +#include + +#include + +namespace SceneUtil +{ + // RTTNode's cull callback + class CullCallback : public osg::NodeCallback + { + public: + CullCallback(RTTNode* group) + : mGroup(group) {} + + void operator()(osg::Node* node, osg::NodeVisitor* nv) override + { + osgUtil::CullVisitor* cv = static_cast(nv); + mGroup->cull(cv); + } + RTTNode* mGroup; + }; + + RTTNode::RTTNode(uint32_t textureWidth, uint32_t textureHeight, int renderOrderNum, bool doPerViewMapping) + : mTextureWidth(textureWidth) + , mTextureHeight(textureHeight) + , mRenderOrderNum(renderOrderNum) + , mDoPerViewMapping(doPerViewMapping) + { + addCullCallback(new CullCallback(this)); + setCullingActive(false); + } + + RTTNode::~RTTNode() + { + } + + void RTTNode::cull(osgUtil::CullVisitor* cv) + { + auto* vdd = getViewDependentData(cv); + apply(vdd->mCamera); + vdd->mCamera->accept(*cv); + } + + osg::Texture* RTTNode::getColorTexture(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mCamera->getBufferAttachmentMap()[osg::Camera::COLOR_BUFFER]._texture; + } + + osg::Texture* RTTNode::getDepthTexture(osgUtil::CullVisitor* cv) + { + return getViewDependentData(cv)->mCamera->getBufferAttachmentMap()[osg::Camera::DEPTH_BUFFER]._texture; + } + + RTTNode::ViewDependentData* RTTNode::getViewDependentData(osgUtil::CullVisitor* cv) + { + if (!mDoPerViewMapping) + // Always setting it to null is an easy way to disable per-view mapping when mDoPerViewMapping is false. + // This is safe since the visitor is never dereferenced. + cv = nullptr; + + if (mViewDependentDataMap.count(cv) == 0) + { + auto camera = new osg::Camera(); + mViewDependentDataMap[cv].reset(new ViewDependentData); + mViewDependentDataMap[cv]->mCamera = camera; + + camera->setRenderOrder(osg::Camera::PRE_RENDER, mRenderOrderNum); + camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + camera->setViewport(0, 0, mTextureWidth, mTextureHeight); + + setDefaults(mViewDependentDataMap[cv]->mCamera.get()); + + // Create any buffer attachments not added in setDefaults + if (camera->getBufferAttachmentMap().count(osg::Camera::COLOR_BUFFER) == 0) + { + auto colorBuffer = new osg::Texture2D; + colorBuffer->setTextureSize(mTextureWidth, mTextureHeight); + colorBuffer->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + colorBuffer->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + colorBuffer->setInternalFormat(GL_RGB); + colorBuffer->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + colorBuffer->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->attach(osg::Camera::COLOR_BUFFER, colorBuffer); + SceneUtil::attachAlphaToCoverageFriendlyFramebufferToCamera(camera, osg::Camera::COLOR_BUFFER, colorBuffer); + } + + if (camera->getBufferAttachmentMap().count(osg::Camera::DEPTH_BUFFER) == 0) + { + auto depthBuffer = new osg::Texture2D; + depthBuffer->setTextureSize(mTextureWidth, mTextureHeight); + depthBuffer->setSourceFormat(GL_DEPTH_COMPONENT); + depthBuffer->setInternalFormat(GL_DEPTH_COMPONENT24); + depthBuffer->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + depthBuffer->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + depthBuffer->setSourceType(GL_UNSIGNED_INT); + depthBuffer->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + depthBuffer->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->attach(osg::Camera::DEPTH_BUFFER, depthBuffer); + } + } + + return mViewDependentDataMap[cv].get(); + } +} diff --git a/components/sceneutil/rtt.hpp b/components/sceneutil/rtt.hpp new file mode 100644 index 000000000..7adfa098e --- /dev/null +++ b/components/sceneutil/rtt.hpp @@ -0,0 +1,68 @@ +#ifndef OPENMW_RTT_H +#define OPENMW_RTT_H + +#include + +#include +#include + +namespace osg +{ + class Texture2D; + class Camera; +} + +namespace osgUtil +{ + class CullVisitor; +} + +namespace SceneUtil +{ + /// @brief Implements per-view RTT operations. + /// @par With a naive RTT implementation, subsequent views of multiple views will overwrite the results of the previous views, leading to + /// the results of the last view being broadcast to all views. An error in all cases where the RTT result depends on the view. + /// @par If using an RTTNode this is solved by mapping RTT operations to CullVisitors, which will be unique per view. This requires + /// instancing one camera per view, and traversing only the camera mapped to that CV during cull traversals. + /// @par Camera settings should be effectuated by overriding the setDefaults() and apply() methods, following a pattern similar to SceneUtil::StateSetUpdater + /// @par When using the RTT texture in your statesets, it is recommended to use SceneUtil::StateSetUpdater as a cull callback to handle this as the appropriate + /// textures can be retrieved during SceneUtil::StateSetUpdater::Apply() + /// @par For any of COLOR_BUFFER or DEPTH_BUFFER not added during setDefaults(), RTTNode will attach a default buffer. The default color buffer has an internal format of GL_RGB. + /// The default depth buffer has internal format GL_DEPTH_COMPONENT24, source format GL_DEPTH_COMPONENT, and source type GL_UNSIGNED_INT. Default wrap is CLAMP_TO_EDGE and filter LINEAR. + class RTTNode : public osg::Node + { + public: + RTTNode(uint32_t textureWidth, uint32_t textureHeight, int renderOrderNum, bool doPerViewMapping); + ~RTTNode(); + + osg::Texture* getColorTexture(osgUtil::CullVisitor* cv); + + osg::Texture* getDepthTexture(osgUtil::CullVisitor* cv); + + + /// Apply state - to override in derived classes + /// @note Due to the view mapping approach you *have* to apply all camera settings, even if they have not changed since the last frame. + virtual void setDefaults(osg::Camera* camera) {}; + + /// Set default settings - optionally override in derived classes + virtual void apply(osg::Camera* camera) {}; + + void cull(osgUtil::CullVisitor* cv); + + private: + struct ViewDependentData + { + osg::ref_ptr mCamera; + }; + + ViewDependentData* getViewDependentData(osgUtil::CullVisitor* cv); + + typedef std::map< osgUtil::CullVisitor*, std::unique_ptr > ViewDependentDataMap; + ViewDependentDataMap mViewDependentDataMap; + uint32_t mTextureWidth; + uint32_t mTextureHeight; + int mRenderOrderNum; + bool mDoPerViewMapping; + }; +} +#endif diff --git a/components/sceneutil/shadow.cpp b/components/sceneutil/shadow.cpp index 3244dc672..761998020 100644 --- a/components/sceneutil/shadow.cpp +++ b/components/sceneutil/shadow.cpp @@ -94,7 +94,8 @@ namespace SceneUtil } } - ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, Shader::ShaderManager &shaderManager) : mShadowedScene(new osgShadow::ShadowedScene), + ShadowManager::ShadowManager(osg::ref_ptr sceneRoot, osg::ref_ptr rootNode, unsigned int outdoorShadowCastingMask, unsigned int indoorShadowCastingMask, Shader::ShaderManager &shaderManager) + : mShadowedScene(new osgShadow::ShadowedScene), mShadowTechnique(new MWShadowTechnique), mOutdoorShadowCastingMask(outdoorShadowCastingMask), mIndoorShadowCastingMask(indoorShadowCastingMask) diff --git a/components/sceneutil/skeleton.cpp b/components/sceneutil/skeleton.cpp index 40f524e0a..8b0947d6a 100644 --- a/components/sceneutil/skeleton.cpp +++ b/components/sceneutil/skeleton.cpp @@ -38,6 +38,7 @@ Skeleton::Skeleton() , mActive(Active) , mLastFrameNumber(0) , mLastCullFrameNumber(0) + , mTracked(false) { } @@ -49,6 +50,7 @@ Skeleton::Skeleton(const Skeleton ©, const osg::CopyOp ©op) , mActive(copy.mActive) , mLastFrameNumber(0) , mLastCullFrameNumber(0) + , mTracked(false) { } @@ -105,7 +107,7 @@ Bone* Skeleton::getBone(const std::string &name) return bone; } -void Skeleton::updateBoneMatrices(unsigned int traversalNumber) +bool Skeleton::updateBoneMatrices(unsigned int traversalNumber) { if (traversalNumber != mLastFrameNumber) mNeedToUpdateBoneMatrices = true; @@ -121,7 +123,9 @@ void Skeleton::updateBoneMatrices(unsigned int traversalNumber) } mNeedToUpdateBoneMatrices = false; + return true; } + return false; } void Skeleton::setActive(ActiveType active) @@ -141,6 +145,11 @@ void Skeleton::markDirty() mBoneCacheInit = false; } +void Skeleton::markBoneMatriceDirty() +{ + mNeedToUpdateBoneMatrices = true; +} + void Skeleton::traverse(osg::NodeVisitor& nv) { if (nv.getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR) diff --git a/components/sceneutil/skeleton.hpp b/components/sceneutil/skeleton.hpp index 22988dfd5..619adfdc8 100644 --- a/components/sceneutil/skeleton.hpp +++ b/components/sceneutil/skeleton.hpp @@ -44,8 +44,8 @@ namespace SceneUtil /// Retrieve a bone by name. Bone* getBone(const std::string& name); - /// Request an update of bone matrices. May be a no-op if already updated in this frame. - void updateBoneMatrices(unsigned int traversalNumber); + /// Request an update of bone matrices. May be a no-op if already updated in this frame. Returns true if update was performed. + bool updateBoneMatrices(unsigned int traversalNumber); enum ActiveType { @@ -64,6 +64,11 @@ namespace SceneUtil void markDirty(); + void markBoneMatriceDirty(); + + void setIsTracked(bool tracked) { mTracked = tracked; } + bool isTracked() const { return mTracked; } + void childInserted(unsigned int) override; void childRemoved(unsigned int, unsigned int) override; @@ -77,6 +82,7 @@ namespace SceneUtil bool mBoneCacheInit; bool mNeedToUpdateBoneMatrices; + bool mTracked; ActiveType mActive; diff --git a/components/sceneutil/statesetupdater.cpp b/components/sceneutil/statesetupdater.cpp index 5d7dbd755..a8232f938 100644 --- a/components/sceneutil/statesetupdater.cpp +++ b/components/sceneutil/statesetupdater.cpp @@ -10,36 +10,57 @@ namespace SceneUtil void StateSetUpdater::operator()(osg::Node* node, osg::NodeVisitor* nv) { bool isCullVisitor = nv->getVisitorType() == osg::NodeVisitor::CULL_VISITOR; - if (!mStateSets[0]) + + if (isCullVisitor) + return applyCull(node, static_cast(nv)); + else + return applyUpdate(node, nv); + } + + void StateSetUpdater::applyUpdate(osg::Node* node, osg::NodeVisitor* nv) + { + if (!mStateSetsUpdate[0]) { - for (int i=0; i<2; ++i) + for (int i = 0; i < 2; ++i) { - if (!isCullVisitor) - mStateSets[i] = new osg::StateSet(*node->getOrCreateStateSet(), osg::CopyOp::SHALLOW_COPY); // Using SHALLOW_COPY for StateAttributes, if users want to modify it is their responsibility to set a non-shared one first in setDefaults - else - mStateSets[i] = new osg::StateSet; - setDefaults(mStateSets[i]); + mStateSetsUpdate[i] = new osg::StateSet(*node->getOrCreateStateSet(), osg::CopyOp::SHALLOW_COPY); // Using SHALLOW_COPY for StateAttributes, if users want to modify it is their responsibility to set a non-shared one first in setDefaults + setDefaults(mStateSetsUpdate[i]); } } - osg::ref_ptr stateset = mStateSets[nv->getTraversalNumber()%2]; + osg::ref_ptr stateset = mStateSetsUpdate[nv->getTraversalNumber() % 2]; apply(stateset, nv); - - if (!isCullVisitor) - node->setStateSet(stateset); - else - static_cast(nv)->pushStateSet(stateset); - + node->setStateSet(stateset); traverse(node, nv); + } + + void StateSetUpdater::applyCull(osg::Node* node, osgUtil::CullVisitor* cv) + { + auto stateset = getCvDependentStateset(cv); + apply(stateset, cv); + cv->pushStateSet(stateset); + traverse(node, cv); + cv->popStateSet(); + } - if (isCullVisitor) - static_cast(nv)->popStateSet(); + osg::StateSet* StateSetUpdater::getCvDependentStateset(osgUtil::CullVisitor* cv) + { + auto it = mStateSetsCull.find(cv); + if (it == mStateSetsCull.end()) + { + osg::ref_ptr stateset = new osg::StateSet; + mStateSetsCull.emplace(cv, stateset); + setDefaults(stateset); + return stateset; + } + return it->second; } void StateSetUpdater::reset() { - mStateSets[0] = nullptr; - mStateSets[1] = nullptr; + mStateSetsUpdate[0] = nullptr; + mStateSetsUpdate[1] = nullptr; + mStateSetsCull.clear(); } StateSetUpdater::StateSetUpdater() diff --git a/components/sceneutil/statesetupdater.hpp b/components/sceneutil/statesetupdater.hpp index 25e50acfd..263f76ae5 100644 --- a/components/sceneutil/statesetupdater.hpp +++ b/components/sceneutil/statesetupdater.hpp @@ -3,6 +3,14 @@ #include +#include +#include + +namespace osgUtil +{ + class CullVisitor; +} + namespace SceneUtil { @@ -11,11 +19,15 @@ namespace SceneUtil /// queues up a StateSet that we want to modify for the next frame. To solve this we could set the StateSet to /// DYNAMIC data variance but that would undo all the benefits of the threading model - having the cull and draw /// traversals run in parallel can yield up to 200% framerates. - /// @par Race conditions are prevented using a "double buffering" scheme - we have two StateSets that take turns, + /// @par Must be set as UpdateCallback or CullCallback on a Node. If set as a CullCallback, the StateSetUpdater operates on an empty StateSet, + /// otherwise it operates on a clone of the node's existing StateSet. + /// @par If set as an UpdateCallback, race conditions are prevented using a "double buffering" scheme - we have two StateSets that take turns, /// one StateSet we can write to, the second one is currently in use by the draw traversal of the last frame. - /// @par Must be set as UpdateCallback or CullCallback on a Node. If set as a CullCallback, the StateSetUpdater operates on an empty StateSet, otherwise it operates on a clone of the node's existing StateSet. + /// @par If set as a CullCallback, race conditions are prevented by mapping statesets to cull visitors - OSG has two cull visitors that take turns, + /// allowing the updater to automatically scale for the number of views. + /// @note When used as a CullCallback, StateSetUpdater will have no effect on leaf nodes such as osg::Geometry and must be used on branch nodes only. /// @note Do not add the same StateSetUpdater to multiple nodes. - /// @note Do not add multiple StateSetControllers on the same Node as they will conflict - instead use the CompositeStateSetUpdater. + /// @note Do not add multiple StateSetUpdaters on the same Node as they will conflict - instead use the CompositeStateSetUpdater. class StateSetUpdater : public osg::NodeCallback { public: @@ -40,7 +52,12 @@ namespace SceneUtil void reset(); private: - osg::ref_ptr mStateSets[2]; + void applyCull(osg::Node* node, osgUtil::CullVisitor* cv); + void applyUpdate(osg::Node* node, osg::NodeVisitor* nv); + osg::StateSet* getCvDependentStateset(osgUtil::CullVisitor* cv); + + std::array, 2> mStateSetsUpdate; + std::map> mStateSetsCull; }; /// @brief A variant of the StateSetController that can be made up of multiple controllers all controlling the same target. diff --git a/components/sceneutil/visitor.hpp b/components/sceneutil/visitor.hpp index 5e041dc45..67b8a375c 100644 --- a/components/sceneutil/visitor.hpp +++ b/components/sceneutil/visitor.hpp @@ -13,8 +13,8 @@ namespace SceneUtil class FindByNameVisitor : public osg::NodeVisitor { public: - FindByNameVisitor(const std::string& nameToFind) - : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) + FindByNameVisitor(const std::string& nameToFind, TraversalMode traversalMode = TRAVERSE_ALL_CHILDREN) + : osg::NodeVisitor(traversalMode) , mNameToFind(nameToFind) , mFoundNode(nullptr) { diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index 43284c216..74abedd6d 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -1,5 +1,7 @@ #include "sdlgraphicswindow.hpp" +#include + #include #ifdef OPENMW_GL4ES_MANUAL_INIT @@ -233,6 +235,23 @@ void GraphicsWindowSDL2::setSyncToVBlank(bool on) SDL_GL_MakeCurrent(oldWin, oldCtx); } +osg::GraphicsContext* GraphicsWindowSDL2::findContext(osgViewer::View& view) +{ + view.getCamera(); + if (view.getCamera()->getGraphicsContext()) + { + return view.getCamera()->getGraphicsContext(); + } + + for (std::size_t i = 0; i < view.getNumSlaves(); i++) + { + if (view.getSlave(i)._camera->getGraphicsContext()) + return view.getSlave(i)._camera->getGraphicsContext(); + } + + return nullptr; +} + void GraphicsWindowSDL2::setSwapInterval(bool enable) { if (enable) diff --git a/components/sdlutil/sdlgraphicswindow.hpp b/components/sdlutil/sdlgraphicswindow.hpp index 559deda67..5c9e992ca 100644 --- a/components/sdlutil/sdlgraphicswindow.hpp +++ b/components/sdlutil/sdlgraphicswindow.hpp @@ -81,6 +81,9 @@ public: SDL_Window *mWindow; }; + /** Convenience function for finding the context among the main camera or slaves */ + static osg::GraphicsContext* findContext(osgViewer::View& view); + private: void setSwapInterval(bool enable); }; diff --git a/components/sdlutil/sdlinputwrapper.cpp b/components/sdlutil/sdlinputwrapper.cpp index 42276cc2a..b55b536c2 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -1,4 +1,5 @@ #include "sdlinputwrapper.hpp" +#include "sdlgraphicswindow.hpp" #include #include @@ -218,7 +219,7 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v SDL_GetWindowSize(mSDLWindow, &w, &h); int x,y; SDL_GetWindowPosition(mSDLWindow, &x,&y); - mViewer->getCamera()->getGraphicsContext()->resized(x,y,w,h); + GraphicsWindowSDL2::findContext(*mViewer)->resized(x,y,w,h); mViewer->getEventQueue()->windowResize(x,y,w,h); diff --git a/components/sdlutil/sdlvideowrapper.cpp b/components/sdlutil/sdlvideowrapper.cpp index b3ba98ee3..acce8f31a 100644 --- a/components/sdlutil/sdlvideowrapper.cpp +++ b/components/sdlutil/sdlvideowrapper.cpp @@ -9,14 +9,16 @@ namespace SDLUtil { - VideoWrapper::VideoWrapper(SDL_Window *window, osg::ref_ptr viewer) + VideoWrapper::VideoWrapper(SDL_Window *window, osg::ref_ptr viewer, bool shouldManageGamma) : mWindow(window) , mViewer(viewer) , mGamma(1.f) , mContrast(1.f) + , mShouldManageGamma(shouldManageGamma) , mHasSetGammaContrast(false) { - SDL_GetWindowGammaRamp(mWindow, mOldSystemGammaRamp, &mOldSystemGammaRamp[256], &mOldSystemGammaRamp[512]); + if(mShouldManageGamma) + SDL_GetWindowGammaRamp(mWindow, mOldSystemGammaRamp, &mOldSystemGammaRamp[256], &mOldSystemGammaRamp[512]); } VideoWrapper::~VideoWrapper() @@ -24,7 +26,7 @@ namespace SDLUtil SDL_SetWindowFullscreen(mWindow, 0); // If user hasn't touched the defaults no need to restore - if (mHasSetGammaContrast) + if (mShouldManageGamma && mHasSetGammaContrast) SDL_SetWindowGammaRamp(mWindow, mOldSystemGammaRamp, &mOldSystemGammaRamp[256], &mOldSystemGammaRamp[512]); } @@ -51,6 +53,9 @@ namespace SDLUtil mHasSetGammaContrast = true; + if (!mShouldManageGamma) + return; + Uint16 red[256], green[256], blue[256]; for (int i = 0; i < 256; i++) { diff --git a/components/sdlutil/sdlvideowrapper.hpp b/components/sdlutil/sdlvideowrapper.hpp index 3866c3ec3..7727852ec 100644 --- a/components/sdlutil/sdlvideowrapper.hpp +++ b/components/sdlutil/sdlvideowrapper.hpp @@ -18,7 +18,7 @@ namespace SDLUtil class VideoWrapper { public: - VideoWrapper(SDL_Window* window, osg::ref_ptr viewer); + VideoWrapper(SDL_Window* window, osg::ref_ptr viewer, bool shouldManageGamma); ~VideoWrapper(); void setSyncToVBlank(bool sync); @@ -35,6 +35,7 @@ namespace SDLUtil float mGamma; float mContrast; + bool mShouldManageGamma; bool mHasSetGammaContrast; // Store system gamma ramp on window creation. Restore system gamma ramp on exit diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 067d34a59..1a7dcdff4 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -10,6 +10,7 @@ namespace Settings CategorySettingValueMap Manager::mDefaultSettings = CategorySettingValueMap(); CategorySettingValueMap Manager::mUserSettings = CategorySettingValueMap(); +CategorySettingValueMap Manager::mSettingsOverrides = CategorySettingValueMap(); CategorySettingVector Manager::mChangedSettings = CategorySettingVector(); void Manager::clear() @@ -17,6 +18,7 @@ void Manager::clear() mDefaultSettings.clear(); mUserSettings.clear(); mChangedSettings.clear(); + mSettingsOverrides.clear(); } /* @@ -33,12 +35,18 @@ void Manager::loadDefault(const std::string &file, bool base64encoded) End of tes3mp change (major) */ -void Manager::loadUser(const std::string &file) +void Manager::loadUser(const std::string& file) { SettingsFileParser parser; parser.loadSettingsFile(file, mUserSettings); } +void Manager::loadOverrides(const std::string& file) +{ + SettingsFileParser parser; + parser.loadSettingsFile(file, mSettingsOverrides); +} + void Manager::saveUser(const std::string &file) { SettingsFileParser parser; @@ -48,7 +56,11 @@ void Manager::saveUser(const std::string &file) std::string Manager::getString(const std::string &setting, const std::string &category) { CategorySettingValueMap::key_type key = std::make_pair(category, setting); - CategorySettingValueMap::iterator it = mUserSettings.find(key); + CategorySettingValueMap::iterator it = mSettingsOverrides.find(key); + if (it != mSettingsOverrides.end()) + return it->second; + + it = mUserSettings.find(key); if (it != mUserSettings.end()) return it->second; @@ -118,8 +130,11 @@ osg::Vec3f Manager::getVector3 (const std::string& setting, const std::string& c void Manager::setString(const std::string &setting, const std::string &category, const std::string &value) { CategorySettingValueMap::key_type key = std::make_pair(category, setting); + CategorySettingValueMap::iterator found = mSettingsOverrides.find(key); + if (found != mSettingsOverrides.end()) + return; - CategorySettingValueMap::iterator found = mUserSettings.find(key); + found = mUserSettings.find(key); if (found != mUserSettings.end()) { if (found->second == value) @@ -187,4 +202,53 @@ void Manager::resetPendingChanges() mChangedSettings.clear(); } + +void Manager::overrideString(const std::string& setting, const std::string& category, const std::string& value) +{ + CategorySettingValueMap::key_type key = std::make_pair(category, setting); + + CategorySettingValueMap::iterator found = mUserSettings.find(key); + if (found != mUserSettings.end()) + { + if (found->second == value) + return; + } + + mSettingsOverrides[key] = value; +} + +void Manager::overrideInt(const std::string& setting, const std::string& category, const int value) +{ + std::ostringstream stream; + stream << value; + overrideString(setting, category, stream.str()); +} + +void Manager::overrideFloat(const std::string& setting, const std::string& category, const float value) +{ + std::ostringstream stream; + stream << value; + overrideString(setting, category, stream.str()); +} + +void Manager::overrideBool(const std::string& setting, const std::string& category, const bool value) +{ + overrideString(setting, category, value ? "true" : "false"); +} + +void Manager::overrideVector2(const std::string& setting, const std::string& category, const osg::Vec2f value) +{ + std::ostringstream stream; + stream << value.x() << " " << value.y(); + overrideString(setting, category, stream.str()); +} + +void Manager::overrideVector3(const std::string& setting, const std::string& category, const osg::Vec3f value) +{ + std::ostringstream stream; + stream << value.x() << ' ' << value.y() << ' ' << value.z(); + overrideString(setting, category, stream.str()); +} + + } diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index 967962c74..fbbf92e1c 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -19,6 +19,7 @@ namespace Settings public: static CategorySettingValueMap mDefaultSettings; static CategorySettingValueMap mUserSettings; + static CategorySettingValueMap mSettingsOverrides; static CategorySettingVector mChangedSettings; ///< tracks all the settings that were changed since the last apply() call @@ -40,6 +41,9 @@ namespace Settings void loadUser (const std::string& file); ///< load file as user settings + void loadOverrides (const std::string& file); + ///< load file as settings overrides + void saveUser (const std::string& file); ///< save user settings to file @@ -65,6 +69,13 @@ namespace Settings static void setBool (const std::string& setting, const std::string& category, const bool value); static void setVector2 (const std::string& setting, const std::string& category, const osg::Vec2f value); static void setVector3 (const std::string& setting, const std::string& category, const osg::Vec3f value); + + static void overrideInt(const std::string& setting, const std::string& category, const int value); + static void overrideFloat(const std::string& setting, const std::string& category, const float value); + static void overrideString(const std::string& setting, const std::string& category, const std::string& value); + static void overrideBool(const std::string& setting, const std::string& category, const bool value); + static void overrideVector2(const std::string& setting, const std::string& category, const osg::Vec2f value); + static void overrideVector3(const std::string& setting, const std::string& category, const osg::Vec3f value); }; } diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 3a5b46440..3a2ed5b8e 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -149,6 +150,109 @@ namespace Shader return true; } + struct DeclarationMeta + { + std::string interpolationType; + std::string interfaceKeyword; + std::string type; + std::string identifier; + std::string mangledIdentifier; + }; + + // Mangle identifiers of the interface declarations of this shader source, updating all identifiers, and returning a list of the declarations for use in generating + // a geometry shader. + // IN/OUT source: The source to mangle + // IN interfaceKeywordPattern: A regular expression matching all interface keywords to look for (e.g. "out|varying" when mangling output variables). Must not contain subexpressions. + // IN mangleString: Identifiers are mangled by prepending this string. Must be a valid identifier prefix. + // OUT declarations: All mangled declarations are added to this vector. Includes interpolation, interface, and type information as well as both the mangled and unmangled identifier. + static void mangleInterface(std::string& source, const std::string& interfaceKeywordPattern, const std::string& mangleString, std::vector& declarations) + { + std::string commentPattern = "//.*"; + std::regex commentRegex(commentPattern); + std::string commentlessSource = std::regex_replace(source, commentRegex, ""); + + std::string identifierPattern = "[a-zA-Z_][0-9a-zA-Z_]*"; + std::string declarationPattern = "(centroid|flat)?\\s*\\b(" + interfaceKeywordPattern + ")\\s+(" + identifierPattern + ")\\s+(" + identifierPattern + ")\\s*;"; + std::regex declarationRegex(declarationPattern); + + std::vector matches(std::sregex_iterator(commentlessSource.begin(), commentlessSource.end(), declarationRegex), std::sregex_iterator()); + std::string replacementPattern; + for (auto& match : matches) + { + declarations.emplace_back(DeclarationMeta{ match[1].str(), match[2].str(), match[3].str(), match[4].str(), mangleString + match[4].str() }); + if (!replacementPattern.empty()) + replacementPattern += "|"; + replacementPattern = replacementPattern + "(" + declarations.back().identifier + "\\b)"; + } + + if (!replacementPattern.empty()) + { + std::regex replacementRegex(replacementPattern); + source = std::regex_replace(source, replacementRegex, mangleString + "$&"); + } + } + + static std::string generateGeometryShader(const std::string& geometryTemplate, const std::vector& declarations) + { + if (geometryTemplate.empty()) + return ""; + + static std::map overriddenForwardStatements = + { + {"linearDepth", "linearDepth = gl_Position.z;"}, + {"euclideanDepth", "euclideanDepth = length(viewPos.xyz);"}, + {"passViewPos", "passViewPos = viewPos.xyz;"}, + { + "screenCoordsPassthrough", + " mat4 scalemat = mat4(0.25, 0.0, 0.0, 0.0,\n" + " 0.0, -0.5, 0.0, 0.0,\n" + " 0.0, 0.0, 0.5, 0.0,\n" + " 0.25, 0.5, 0.5, 1.0);\n" + " vec4 texcoordProj = ((scalemat) * (gl_Position));\n" + " screenCoordsPassthrough = texcoordProj.xyw;\n" + " if(viewport == 1)\n" + " screenCoordsPassthrough.x += 0.5 * screenCoordsPassthrough.z;\n" + } + }; + + std::stringstream ssInputDeclarations; + std::stringstream ssOutputDeclarations; + std::stringstream ssForwardStatements; + std::stringstream ssExtraStatements; + std::set identifiers; + for (auto& declaration : declarations) + { + if (!declaration.interpolationType.empty()) + { + ssInputDeclarations << declaration.interpolationType << " "; + ssOutputDeclarations << declaration.interpolationType << " "; + } + ssInputDeclarations << "in " << declaration.type << " " << declaration.mangledIdentifier << "[3];\n"; + ssOutputDeclarations << "out " << declaration.type << " " << declaration.identifier << ";\n"; + + if (overriddenForwardStatements.count(declaration.identifier) > 0) + ssForwardStatements << overriddenForwardStatements[declaration.identifier] << ";\n"; + else + ssForwardStatements << " " << declaration.identifier << " = " << declaration.mangledIdentifier << "[vertex];\n"; + + identifiers.insert(declaration.identifier); + } + + // passViewPos output is required + if (identifiers.find("passViewPos") == identifiers.end()) + { + Log(Debug::Error) << "Vertex shader is missing 'vec3 passViewPos' on its interface. Geometry shader will NOT work."; + return ""; + } + + std::string geometryShader = geometryTemplate; + geometryShader = std::regex_replace(geometryShader, std::regex("@INPUTS"), ssInputDeclarations.str()); + geometryShader = std::regex_replace(geometryShader, std::regex("@OUTPUTS"), ssOutputDeclarations.str()); + geometryShader = std::regex_replace(geometryShader, std::regex("@FORWARDING"), ssForwardStatements.str()); + + return geometryShader; + } + bool parseFors(std::string& source, const std::string& templateName) { const char escapeCharacter = '$'; @@ -187,7 +291,7 @@ namespace Shader std::string list = source.substr(listStart, listEnd - listStart); std::vector listElements; if (list != "") - Misc::StringUtils::split (list, listElements, ","); + Misc::StringUtils::split(list, listElements, ","); size_t contentStart = source.find_first_not_of("\n\r", listEnd); size_t contentEnd = source.find("$endforeach", contentStart); @@ -246,7 +350,7 @@ namespace Shader Log(Debug::Error) << "Shader " << templateName << " error: Unexpected EOF"; return false; } - std::string define = source.substr(foundPos+1, endPos - (foundPos+1)); + std::string define = source.substr(foundPos + 1, endPos - (foundPos + 1)); ShaderManager::DefineMap::const_iterator defineFound = defines.find(define); ShaderManager::DefineMap::const_iterator globalDefineFound = globalDefines.find(define); if (define == "foreach") @@ -293,39 +397,15 @@ namespace Shader return true; } - osg::ref_ptr ShaderManager::getShader(const std::string &templateName, const ShaderManager::DefineMap &defines, osg::Shader::Type shaderType) + osg::ref_ptr ShaderManager::getShader(const std::string& templateName, const ShaderManager::DefineMap& defines, osg::Shader::Type shaderType) { std::lock_guard lock(mMutex); - // read the template if we haven't already - TemplateMap::iterator templateIt = mShaderTemplates.find(templateName); - if (templateIt == mShaderTemplates.end()) - { - boost::filesystem::path path = (boost::filesystem::path(mPath) / templateName); - boost::filesystem::ifstream stream; - stream.open(path); - if (stream.fail()) - { - Log(Debug::Error) << "Failed to open " << path.string(); - return nullptr; - } - std::stringstream buffer; - buffer << stream.rdbuf(); - - // parse includes - int fileNumber = 1; - std::string source = buffer.str(); - if (!addLineDirectivesAfterConditionalBlocks(source) - || !parseIncludes(boost::filesystem::path(mPath), source, templateName, fileNumber, {})) - return nullptr; - - templateIt = mShaderTemplates.insert(std::make_pair(templateName, source)).first; - } - ShaderMap::iterator shaderIt = mShaders.find(std::make_pair(templateName, defines)); if (shaderIt == mShaders.end()) { - std::string shaderSource = templateIt->second; + std::string shaderSource = getTemplateSource(templateName); + if (!parseDefines(shaderSource, defines, mGlobalDefines, templateName) || !parseFors(shaderSource, templateName)) { // Add to the cache anyway to avoid logging the same error over and over. @@ -333,12 +413,32 @@ namespace Shader return nullptr; } - osg::ref_ptr shader (new osg::Shader(shaderType)); - shader->setShaderSource(shaderSource); + osg::ref_ptr shader(new osg::Shader(shaderType)); // Assign a unique name to allow the SharedStateManager to compare shaders efficiently static unsigned int counter = 0; shader->setName(std::to_string(counter++)); + if (mGeometryShadersEnabled && defines.count("geometryShader") && defines.find("geometryShader")->second == "1" && shaderType == osg::Shader::VERTEX) + { + std::vector declarations; + mangleInterface(shaderSource, "out|varying", "vertex_", declarations); + std::string geometryTemplate = getTemplateSource("stereo_geometry.glsl"); + std::string geometryShaderSource = generateGeometryShader(geometryTemplate, declarations); + if (!geometryShaderSource.empty()) + { + osg::ref_ptr geometryShader(new osg::Shader(osg::Shader::GEOMETRY)); + geometryShader->setShaderSource(geometryShaderSource); + geometryShader->setName(shader->getName() + ".geom"); + mGeometryShaders[shader] = geometryShader; + } + else + { + Log(Debug::Error) << "Failed to generate geometry shader for " << templateName; + } + } + + shader->setShaderSource(shaderSource); + shaderIt = mShaders.insert(std::make_pair(std::make_pair(templateName, defines), shader)).first; } return shaderIt->second; @@ -350,11 +450,18 @@ namespace Shader ProgramMap::iterator found = mPrograms.find(std::make_pair(vertexShader, fragmentShader)); if (found == mPrograms.end()) { - osg::ref_ptr program (new osg::Program); + osg::ref_ptr program(new osg::Program); program->addShader(vertexShader); program->addShader(fragmentShader); program->addBindAttribLocation("aOffset", 6); program->addBindAttribLocation("aRotation", 7); + + auto git = mGeometryShaders.find(vertexShader); + if (git != mGeometryShaders.end()) + { + program->addShader(git->second); + } + if (mLightingMethod == SceneUtil::LightingMethod::SingleUBO) program->addBindUniformBlock("LightBufferBinding", static_cast(UBOBinding::LightBuffer)); found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; @@ -367,10 +474,20 @@ namespace Shader return DefineMap(mGlobalDefines); } - void ShaderManager::setGlobalDefines(DefineMap & globalDefines) + void ShaderManager::setStereoGeometryShaderEnabled(bool enabled) + { + mGeometryShadersEnabled = enabled; + } + + bool ShaderManager::stereoGeometryShaderEnabled() const + { + return mGeometryShadersEnabled; + } + + void ShaderManager::setGlobalDefines(DefineMap& globalDefines) { mGlobalDefines = globalDefines; - for (auto shaderMapElement: mShaders) + for (auto shaderMapElement : mShaders) { std::string templateId = shaderMapElement.first.first; ShaderManager::DefineMap defines = shaderMapElement.first.second; @@ -387,7 +504,7 @@ namespace Shader } } - void ShaderManager::releaseGLObjects(osg::State *state) + void ShaderManager::releaseGLObjects(osg::State* state) { std::lock_guard lock(mMutex); for (auto shader : mShaders) @@ -399,4 +516,33 @@ namespace Shader program.second->releaseGLObjects(state); } + std::string ShaderManager::getTemplateSource(const std::string& templateName) + { + // read the template if we haven't already + TemplateMap::iterator templateIt = mShaderTemplates.find(templateName); + if (templateIt == mShaderTemplates.end()) + { + boost::filesystem::path path = (boost::filesystem::path(mPath) / templateName); + boost::filesystem::ifstream stream; + stream.open(path); + if (stream.fail()) + { + Log(Debug::Error) << "Failed to open " << path.string(); + return std::string(); + } + std::stringstream buffer; + buffer << stream.rdbuf(); + + // parse includes + int fileNumber = 1; + std::string source = buffer.str(); + if (!addLineDirectivesAfterConditionalBlocks(source) + || !parseIncludes(boost::filesystem::path(mPath), source, templateName, fileNumber, {})) + return std::string(); + + templateIt = mShaderTemplates.insert(std::make_pair(templateName, source)).first; + } + return templateIt->second; + } + } diff --git a/components/shader/shadermanager.hpp b/components/shader/shadermanager.hpp index 2450f0d6d..6d12a7078 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -58,6 +58,14 @@ namespace Shader /// Get (a copy of) the DefineMap used to construct all shaders DefineMap getGlobalDefines(); + /// Enable or disable automatic stereo geometry shader. + /// If enabled, a stereo geometry shader will be automatically generated for any vertex shader + /// whose defines include "geometryShader" set to "1". + /// This geometry shader is automatically included in any program using that vertex shader. + /// \note Does not affect programs that have already been created, set this during startup. + void setStereoGeometryShaderEnabled(bool enabled); + bool stereoGeometryShaderEnabled() const; + /// Set the DefineMap used to construct all shaders /// @param defines The DefineMap to use /// @note This will change the source code for any shaders already created, potentially causing problems if they're being used to render a frame. It is recommended that any associated Viewers have their threading stopped while this function is running if any shaders are in use. @@ -66,6 +74,8 @@ namespace Shader void releaseGLObjects(osg::State* state); private: + std::string getTemplateSource(const std::string& templateName); + std::string mPath; DefineMap mGlobalDefines; @@ -78,6 +88,10 @@ namespace Shader typedef std::map > ShaderMap; ShaderMap mShaders; + typedef std::map, osg::ref_ptr > GeometryShaderMap; + GeometryShaderMap mGeometryShaders; + bool mGeometryShadersEnabled{ false }; + typedef std::map, osg::ref_ptr >, osg::ref_ptr > ProgramMap; ProgramMap mPrograms; diff --git a/components/shader/shadervisitor.cpp b/components/shader/shadervisitor.cpp index 9550c903a..8edb51f95 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -473,6 +473,7 @@ namespace Shader } defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0"; + defineMap["geometryShader"] = "1"; writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode)); addedState->addUniform("colorMode"); diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index e662f4439..0062825ba 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -232,6 +232,7 @@ namespace Terrain defineMap["blendMap"] = (!blendmaps.empty()) ? "1" : "0"; defineMap["specularMap"] = it->mSpecular ? "1" : "0"; defineMap["parallax"] = (it->mNormalMap && it->mParallax) ? "1" : "0"; + defineMap["geometryShader"] = "1"; osg::ref_ptr vertexShader = shaderManager->getShader("terrain_vertex.glsl", defineMap, osg::Shader::VERTEX); osg::ref_ptr fragmentShader = shaderManager->getShader("terrain_fragment.glsl", defineMap, osg::Shader::FRAGMENT); diff --git a/components/widgets/box.cpp b/components/widgets/box.cpp index 3e8f62b4b..41c6871c8 100644 --- a/components/widgets/box.cpp +++ b/components/widgets/box.cpp @@ -1,4 +1,5 @@ #include "box.hpp" +#include "virtualkeyboardmanager.hpp" #include @@ -486,4 +487,32 @@ namespace Gui setUserString("VStretch", "true"); } + + EditBox::EditBox() + : mVirtualKeyboardRegistered(false) + { + registerVirtualKeyboard(); + } + EditBox::~EditBox() + { + unregisterVirtualKeyboard(); + } + void EditBox::registerVirtualKeyboard() + { + auto* vkm = Gui::VirtualKeyboardManager::getInstancePtr(); + if (vkm) + { + vkm->registerEditBox(this); + mVirtualKeyboardRegistered = true; + } + } + void EditBox::unregisterVirtualKeyboard() + { + if (mVirtualKeyboardRegistered) + { + // No need to check here + Gui::VirtualKeyboardManager::getInstance().unregisterEditBox(this); + mVirtualKeyboardRegistered = false; + } + } } diff --git a/components/widgets/box.hpp b/components/widgets/box.hpp index 60d0ea67a..8d02ca97a 100644 --- a/components/widgets/box.hpp +++ b/components/widgets/box.hpp @@ -24,6 +24,16 @@ namespace Gui class EditBox : public FontWrapper { MYGUI_RTTI_DERIVED( EditBox ) + + /// @param supportsVirtualKeyboard If true, VR mode will spawn a virtual keyboard whenever this widget is focused. + EditBox(); + ~EditBox(); + + private: + void registerVirtualKeyboard(); + void unregisterVirtualKeyboard(); + + bool mVirtualKeyboardRegistered; }; class AutoSizedWidget diff --git a/components/widgets/numericeditbox.hpp b/components/widgets/numericeditbox.hpp index e1f33e887..90805c04a 100644 --- a/components/widgets/numericeditbox.hpp +++ b/components/widgets/numericeditbox.hpp @@ -3,7 +3,7 @@ #include -#include "fontwrapper.hpp" +#include "box.hpp" namespace Gui { @@ -11,7 +11,7 @@ namespace Gui /** * @brief A variant of the EditBox that only allows integer inputs */ - class NumericEditBox final : public FontWrapper + class NumericEditBox final : public Gui::EditBox { MYGUI_RTTI_DERIVED(NumericEditBox) diff --git a/components/widgets/virtualkeyboardmanager.cpp b/components/widgets/virtualkeyboardmanager.cpp new file mode 100644 index 000000000..8a1b45060 --- /dev/null +++ b/components/widgets/virtualkeyboardmanager.cpp @@ -0,0 +1,7 @@ +#include "virtualkeyboardmanager.hpp" + +template<> +Gui::VirtualKeyboardManager* MyGUI::Singleton::msInstance = nullptr; + +template<> +const char* MyGUI::Singleton::mClassTypeName = "Gui::VirtualKeyboardManager"; diff --git a/components/widgets/virtualkeyboardmanager.hpp b/components/widgets/virtualkeyboardmanager.hpp new file mode 100644 index 000000000..f8335294a --- /dev/null +++ b/components/widgets/virtualkeyboardmanager.hpp @@ -0,0 +1,18 @@ +#ifndef OPENMW_WIDGETS_VIRTUALKEYBOARDMANAGER_H +#define OPENMW_WIDGETS_VIRTUALKEYBOARDMANAGER_H + +#include +#include "MyGUI_Singleton.h" + +namespace Gui +{ + class VirtualKeyboardManager : + public MyGUI::Singleton + { + public: + virtual void registerEditBox(MyGUI::EditBox* editBox) = 0; + virtual void unregisterEditBox(MyGUI::EditBox* editBox) = 0; + }; +} + +#endif diff --git a/docs/controller_graphics/Oculus_Touch.png b/docs/controller_graphics/Oculus_Touch.png new file mode 100644 index 000000000..dfc958b7f Binary files /dev/null and b/docs/controller_graphics/Oculus_Touch.png differ diff --git a/docs/controller_graphics/Valve_Index.png b/docs/controller_graphics/Valve_Index.png new file mode 100644 index 000000000..4677dc2a6 Binary files /dev/null and b/docs/controller_graphics/Valve_Index.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py index 7653b94ed..618132ff5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -147,7 +147,7 @@ html_theme = 'sphinx_rtd_theme' #html_css_files = 'figures.css' use this once Sphinx 1.8 is released!!! def setup(app): - app.add_stylesheet('figures.css') + app.add_css_file('figures.css') # The name for this set of Sphinx documents. If None, it defaults to diff --git a/docs/source/manuals/index.rst b/docs/source/manuals/index.rst index e6f0cbef2..a0c6cf356 100644 --- a/docs/source/manuals/index.rst +++ b/docs/source/manuals/index.rst @@ -4,5 +4,6 @@ User Manuals .. toctree:: :maxdepth: 2 + openmw-vr/index openmw-cs/index installation/index diff --git a/docs/source/manuals/installation/index.rst b/docs/source/manuals/installation/index.rst index 6e6f5034e..b0b5987a3 100644 --- a/docs/source/manuals/installation/index.rst +++ b/docs/source/manuals/installation/index.rst @@ -8,5 +8,6 @@ In order to use OpenMW, you must install both the engine and the game files for :maxdepth: 2 install-openmw + install-openmw-vr install-game-files common-problems \ No newline at end of file diff --git a/docs/source/manuals/installation/install-openmw-vr.rst b/docs/source/manuals/installation/install-openmw-vr.rst new file mode 100644 index 000000000..67adefaad --- /dev/null +++ b/docs/source/manuals/installation/install-openmw-vr.rst @@ -0,0 +1,47 @@ +================= +Install OpenMW-VR +================= + +OpenMW-VR is not yet a part of official OpenMW and must be installed separately. + + .. note:: + OpenMW-VR is currently a development fork separate from OpenMW, and is without + an official release schedule. You will be installing development builds. Tags, + for those who know what that is, are intermittent and semi-arbitrary, and no + official "release" has been made. + + .. note:: + Installing OpenMW (the non-VR version) is not required. If you have not run the + openmw installer, simply run the wizard included with the OpenMW-VR install. + +Windows Binaries +================ + +If you're not sure what any of the different methods mean, you should probably stick to this one. +Simply download the latest build from the following link: +`https://gitlab.com/madsbuvi/openmw/-/jobs/artifacts/openmw-vr/download?job=Windows_Ninja_Engine_Release `_ +and extract the archive into a new/empty folder. If you have not run the OpenMW install wizard in the past, run the extracted openmw_wizard.exe. Now you can run the extracted openmw-vr.exe to launch the game. + + .. note:: + There is no need to uninstall previous builds, but extract each build into a **new or empty** folder and **do not** extract two builds to the same folder. + +Linux +===== + +Linux users will have to build from source. + + .. note:: + Unlike OpenMW, OpenMW-VR has bumped the version requirement of OSG to 3.6.5. On some distros, + this means you'll have to roll your own OSG instead of using what's in the official repos. + + .. note:: + I do not maintain this fork on linux, other than fixing compilation errors caught by the CI. + I depend on others to make PRs for anything specific to Linux. + +From Source +=========== + +Visit the `Development Environment Setup `_ +section of the Wiki for detailed instructions on how to build the engine. + +Clone the OpenMW-VR repo at https://gitlab.com/madsbuvi/openmw instead of the regular OpenMW repo, and checkout the branch openmw-vr (this should get checked out by default). diff --git a/docs/source/manuals/openmw-vr/common-problems.rst b/docs/source/manuals/openmw-vr/common-problems.rst new file mode 100644 index 000000000..180de44bc --- /dev/null +++ b/docs/source/manuals/openmw-vr/common-problems.rst @@ -0,0 +1,78 @@ +############### +Common Problems +############### + +Bad performance +############### + +:Symptoms: + OpenMW-VR plays with a very low framerate. + +:Cause: + OpenMW-VR is based on OpenMW which is not super well optimized, and is being used to render very poorly optimized + assets. VR on top of this can be extremely slow when heavy features like shadows and water reflections are enabled. + +:Fix: + Reduce shadow complexity or disable shadows altogether. Reduce water shader complexity or disable it altogether. + Make sure "Use shared shadow maps" is enabled in the launcher (launcher->advanced->VR). + +My Controller doesn't work +########################## + +:Symptoms: + OpenMW-VR appears in my headset but my motion controllers do nothing or have extremely limited functionality. + Unable to complete even basic gameplay. + +:Cause 1: + No profile exists for that controller. Check xrcontrollersuggestions.xml and see if there is a profile entry for it. + If there is none, then OpenMW-VR has not added support for your controller yet. + +:Fix 1: + Write your own profile. Consult the openxr spec for the exact interction profile Path, extension name, and available + action paths. Feel free to send me this profile, either in an issue or a MR. + +:Fix 2: + Open an issue at the gitlab and i'll add a profile. Not that i don't have your controller and may write a suboptimal profile. + +:Cause 2: + You are using an openxr runtime that does not support your controllers. For example, last I checked SteamVR did not + support the vive cosmos controllers. If this is the cause then a line in openmw.log will state ``Configuring interaction profile '/interaction_profiles/vendor/controller' (Controller Name)``. + followed by ``Required extension 'XR_#' not supported. Skipping interaction profile.``. + +:Fix: + Make sure your runtime is up to date. Try switching to your headset/controller's native runtimes, as opposed to a + non-native runtime like SteamVR. + +The menu button and thumbsticks don't work. +########################################### + +:Symptoms: + Motion controllers work but thumbsticks and the menu button do nothing. + +:Cause: + Known bug in SteamVR. + +:Fix: + Use your headset's native runtimes instead of SteamVR. + +Audio does not go to my VR Headset +################################## + +:Symptoms: + Audio is not automatically playing in your VR headset instead of your default speakers. + +:Cause: + OpenMW explicitly selects an audio device at startup and sticks with it. By default this is the default speaker. + +:Fix: + Either set your default audio to the VR headset, or configure openmw to use it. To do this, add the following lines + to the end of settings.cfg. + + :: + + [Sound] + device = # + + Where # is the identifier of your device. You can find the correct identifier by reading openmw.log, look for + the line ``Enumerated output devices:`` where devices are enumerated. + diff --git a/docs/source/manuals/openmw-vr/controls.rst b/docs/source/manuals/openmw-vr/controls.rst new file mode 100644 index 000000000..aa1d6e4f6 --- /dev/null +++ b/docs/source/manuals/openmw-vr/controls.rst @@ -0,0 +1,108 @@ +Controls +######## + +TODO: It would be useful to include a **brief** gameplay video demonstrating the VR interface here. + +Default Bindings +**************** + +Default bindings exist for the following controllers: + - Oculus Touch Controllers + - Index Knuckles + - Vive Wands + - Microsoft Motion Controllers + - Hp Mixed Reality Controllers + - Huawei Controllers + - Vive Cosmos Controllers + +Oculus Touch controllers default bindings: + .. image:: ../../../controller_graphics/Oculus_Touch.png + :width: 600 +Valve Index controllers default bindings: + .. image:: ../../../controller_graphics/Valve_Index.png + :width: 600 + +Similar graphics for other controllers will be added if people with those bindings contribute any. +Default controls for other controllers are written to be similar to the touch and index controls. + +Rebinding +********* +Currently there is no user friendly rebinding system available. +Some OpenXR runtimes may offer their own rebinding service similar to what SteamVR already offers for OpenVR applications. + +If you can't rely on that, you'll have to edit +the bindings manually. To do so find the *xrcontrollersuggestions.xml* file in your openmw root folder **and copy it to +documents/my games/openmw/** or your platform's equivalent, and edit the new copy. +The file itself contains a brief explanation for how to edit the file. + + .. note:: You are kindly requested not to edit the xrcontrollersuggestions.xml file that resides in the installation folder. If you do you will not receive any support until you restore the original by reinstalling. + + .. note:: If your OpenXR runtime sports its own rebinding service, it's possible that changes to xrcontrollersuggestions.xml will have no effect and you'll be forced to use that service. + +Actions Sets +************ + +Actions are divided into two bindable sets: Gameplay, and GUI. + +The GUI action set is active whenever you are in a menu and gameplay is paused. +The gameplay action set is active whenever not in a menu. Bindings may therefore overlap between the two. + +Gameplay: + - reposition_menu (aka recenter) + - meta_menu + - sneak + - always_run + - jump + - spell (aka ready_spell) + - weapon (aka ready_weapon) + - rest + - inventory + - activate + - activate_touched + - auto_move + - use + - move_left_right + - move_forward_backward + - look_left_right + +GUI: + - menu_up_down + - menu_left_right + - menu_select + - menu_back + - use + - game_menu + - reposition_menu + +Most of these actions are self-explanatory with a few exceptions. + +:activate_touched: + Whenever this control is active, pointer mode is enabled and your finger will point at stuff. Realistic combat is + disabled in this mode, so avoid bindings that are easy to activate unintentionally. + +:use: + The Use action in VR combines the Active and Use action of pancake OpenMW. When pointer mode is active, Use will + activate whatever you are pointing at. When pointer mode is not active, Use will use the readied tool/spell in the + direction you are orienting it. + +:Recenter: + The special action Recenter is by default assigned the same button as "Meta Menu". To activate recentering, you must **press and hold** the assigned button. + The recenter action has differing behaviour depending on whether you are currently in a menu, seated play, or standing play. + + .. note:: In the xrcontrollersuggestions.xml file the recenter action is labeled "reposition_menu". + +Recenter (Menus) +**************** + +If recentering while navigating some menu, all windows are moved to center on your current location/orientation. +This is useful if a menu ended up inside of some geometry. + +Recenter (Standing play) +************************ + +If recentering while in standing play, your view is moved to the location of your character **horizontally** + +Recenter (Seated play) +********************** + +If recentering while in seated play, your view is moved to the location of your character **horizontally and vertically**. \ No newline at end of file diff --git a/docs/source/manuals/openmw-vr/foreword.rst b/docs/source/manuals/openmw-vr/foreword.rst new file mode 100644 index 000000000..c4104ca8b --- /dev/null +++ b/docs/source/manuals/openmw-vr/foreword.rst @@ -0,0 +1,37 @@ +Foreword +######## + +How to read the manual +********************** + +The manual can be roughly divided into two parts: a tutorial part that will +introduce you to the VR interface and what the available options are, and +a pitfalls part that will explain (current) known shortcomings of the VR port +and pitfalls of VR in general. + +The rest of this page will explain some terminology. + +Terminology +*********** +A brief explanation of terms and abbreviation + - VR: Virtual Reality (duh) + - VR Stage: Your physical play area + - Mirror texture: The motion picture shown on your pancake monitor when you are playing in VR + - OpenXR: An Open Source interface for accessing AR/VR devices. + + This is the VR equivalent of Vulkan, developed by the same group as Vulkan: Khronos. OpenXR, like Vulkan, is purely an interface and not an implementation. + - OpenXR Runtime: An implementation of the OpenXR standard. + - VR Runtime: The software interfacing with your VR device. An OpenXR Runtime is a subset of a VR runtime. + - Native Runtimes: The VR Runtime provided by your device drivers. + - SteamVR: Valve's VR Runtime, shipped as a part of Steam. SteamVR, native to the Index, acts also as a non-native runtime to all other headsets + +OpenXR vs What you're used to +***************************** +Before moving on to the manual contents, I would like to note that OpenMW-VR uses the **OpenXR** API to access VR. + +Most VR games you've ever played use vendor-specific APIs, such as OpenVR (SteamVR), Oculus VR, WMR, etc. and not OpenXR. +OpenXR is very new, with most vendors only releasing stable implementations this year (2021). +This means that while playing OpenMW-VR you may see bugs originating from your VR runtime, that you don't see in other games. +This will diminish in time as OpenXR runtimes mature. + +As an example, SteamVR's OpenXR runtime neglects to map some controls (thumbsticks and the menu button), rendering them inoperable. diff --git a/docs/source/manuals/openmw-vr/getting_started.rst b/docs/source/manuals/openmw-vr/getting_started.rst new file mode 100644 index 000000000..027da4eb6 --- /dev/null +++ b/docs/source/manuals/openmw-vr/getting_started.rst @@ -0,0 +1,72 @@ +Getting Started +############### + +Installation +************ +Navigate to the Installation Guide on the left for help with installing OpenMW-VR. + +Before Launching +**************** +Before launching, you should run the openmw-launcher from your openmw-vr dir and go to the advaced->VR tab. +Here you should input your real height in meters. There are some other options here that you don't need to care about, +except maybe the melee combat swing speed if the default doesn't work well for you. + +In-game settings +**************** +In addition to the launcher settings, there are some in-game settings to be aware of. You should look through these +to make sure everything is to your liking. + +:Enable mirror texture: + If disabled, nothing is shown on your pancake monitor. +:Mirror texture eye: + Determines which of your eyes is shown on your pancake monitor. +:Flip mirror texture order: + If showing both eyes, flip their order. +:Haptic Feedback: + If enabled, controllers will vibrate in response to successful hits, and to being hit. +:Hand Directed Movement: + If enabled, movement is directed by your left hand instead of your current head orientation. +:Seated Play: + If enabled, tracking is adjusted for seated play. +:Left Hand Hud Position: + Presents two options for where to position the left hand hud. Two options exist since the one i originally made + worked very poorly with shields. + +Gameplay +******** +TODO: This part would probably be best demonstrated with a *brief* in-game video. +The core of the gameplay is the same as pancake OpenMW. The core difference is that instead of pointing and clicking with +a mouse, you'll be pointing and clicking with motion controllers. + +:activators: + To interact with activators of any kind (doors, npcs, chests, etc), enable pointer mode using right grip/squeeze + and press the trigger when the pointer beam is on an activator. + +:text input: + Whenever a text input dialogue appears, look at your left hand and write your name using the + virtual keyboard attached to your left hand. + +:realistic melee combat: + Melee combat in openmw-vr is "realistic", meaning you can swing your sword at your foes to hurt them. Currently + the only way to disambiguate a dice roll miss from an actual miss, is to see if the enemy's healthbar appeared + on your left-hand hud. There is a minimum sing speed below which swings are not activated, and a maximum swing speed + above which no benefit is derived. Swing strength scales from minimum to maximum in this interval. Openmw-vr selects + between chop, slash, and thrust based on how you moved your weapon. + + .. note:: Although there is no explicit option to disable realistic combat, you can effectively disabled it by + entering unreasonably high values for swing speeds. + +:unrealistic melee combat: + If you prefer not swinging your weapon, point and click combat is possible by holding your weapon towards the enemy + and pressing the right trigger. This mode does not receive any animation. Note that you must aim your weapon, not + your hand, at the enemy. + +:lockpicks and probes: + Lockpicks and propes work like unrealistic melee combat. Aim your tool at the door/chest and press use. + +:On-touch spells: + Simply point your hand at your target and press use. + +:Ranged combat: + Ranged combat is a simple point and click. Aiming works the same for all ranged, including spells. No aim assistance + has been implemented. diff --git a/docs/source/manuals/openmw-vr/index.rst b/docs/source/manuals/openmw-vr/index.rst new file mode 100644 index 000000000..3839eb219 --- /dev/null +++ b/docs/source/manuals/openmw-vr/index.rst @@ -0,0 +1,25 @@ +OpenMW VR User Manual +##################### + +The following document is the complete user manual for *OpenMW VR*, the +VR port of the OpenMW game engine. It is intended to serve both as an +introduction and a reference for the application. Even if you are familiar with +*OpenMW* and/or *The Elder Scrolls III: Morrowind* you should at least read the first +few chapters to familiarise yourself with the VR interface. + +.. warning:: + OpenMW VR is still software in development. The manual does not cover any of + its shortcomings, it is written as if everything was working as intended. + Please report any software problems as bugs in the software, not errors in + the manual. + +.. toctree:: + :caption: Table of Contents + :maxdepth: 2 + + foreword + getting_started + controls + modding + versioning + common-problems diff --git a/docs/source/manuals/openmw-vr/modding.rst b/docs/source/manuals/openmw-vr/modding.rst new file mode 100644 index 000000000..4c6939503 --- /dev/null +++ b/docs/source/manuals/openmw-vr/modding.rst @@ -0,0 +1,9 @@ +Modding +####### +This section will detail any differences in mods and modding between VR and pancake. +Currently OpenMW-VR is not known to break any mods that work in an equivalent version of OpenMW. + +MyGUI resource files +******************** +The GUI of OpenMW is rendered using MyGUI resource files located in resources/mygui/. In OpenMW-VR Some of these +differ in content/name from OpenMW and any mods that mod the UI by modifying these files may break. \ No newline at end of file diff --git a/docs/source/manuals/openmw-vr/versioning.rst b/docs/source/manuals/openmw-vr/versioning.rst new file mode 100644 index 000000000..48cdbccf0 --- /dev/null +++ b/docs/source/manuals/openmw-vr/versioning.rst @@ -0,0 +1,14 @@ +Versions and Releases +##################### +OpenMW-VR does not have its own versioning. OpenMW-VR does however base itself on builds of OpenMW. Currently this is +an arbitrarily chosen nightly build of OpenMW 0.47. + +Release Schedule +**************** +Currently OpenMW-VR does not have anything like a release schedule or official version numbers. New builds happen +whenever I find the time to do some coding. This is because the intent is to merge with OpenMW and make VR an official, +optional feature of OpenMW, not to maintain a separate project. + +Merge Timeline: 0.48 +******************** +Currently, the intent is to merge OpenMW-VR by 0.48. But this is not a promise. \ No newline at end of file diff --git a/files/mygui/CMakeLists.txt b/files/mygui/CMakeLists.txt index 3ff8a1b2b..96f0a5abf 100644 --- a/files/mygui/CMakeLists.txt +++ b/files/mygui/CMakeLists.txt @@ -9,14 +9,15 @@ set(DDIRRELATIVE resources/mygui) set(MYGUI_FILES core.skin core.xml + core_vr.xml core_layouteditor.xml openmw_alchemy_window.layout openmw_book.layout openmw_box.skin.xml openmw_button.skin.xml openmw_chargen_birth.layout - openmw_chargen_class_description.layout openmw_chargen_class.layout + openmw_chargen_class_description.layout openmw_chargen_create_class.layout openmw_chargen_generate_class_result.layout openmw_chargen_race.layout @@ -24,72 +25,85 @@ set(MYGUI_FILES openmw_chargen_select_attribute.layout openmw_chargen_select_skill.layout openmw_chargen_select_specialization.layout + openmw_companion_window.layout openmw_confirmation_dialog.layout openmw_console.layout openmw_console.skin.xml openmw_container_window.layout + openmw_container_window_vr.layout openmw_count_window.layout + openmw_debug_window.layout + openmw_debug_window.skin.xml openmw_dialogue_window.layout + openmw_dialogue_window_vr.layout openmw_dialogue_window.skin.xml openmw_edit.skin.xml + openmw_edit_effect.layout + openmw_edit_note.layout + openmw_enchanting_dialog.layout openmw_font.xml + openmw_hud.layout openmw_hud_box.skin.xml openmw_hud_energybar.skin.xml - openmw_hud.layout + openmw_hud_vr.layout openmw_infobox.layout openmw_interactive_messagebox.layout openmw_interactive_messagebox_notransp.layout openmw_inventory_window.layout + openmw_inventory_window_vr.layout + openmw_itemselection_dialog.layout + openmw_jail_screen.layout openmw_journal.layout openmw_journal.skin.xml openmw_layers.xml + openmw_layers_vr.xml + openmw_levelup_dialog.layout openmw_list.skin.xml + openmw_loading_screen.layout + openmw_magicselection_dialog.layout + openmw_vr_metamenu.layout openmw_mainmenu.layout openmw_mainmenu.skin.xml openmw_map_window.layout openmw_map_window.skin.xml + openmw_map_window_vr.layout + openmw_merchantrepair.layout openmw_messagebox.layout + openmw_persuasion_dialog.layout openmw_pointer.xml openmw_progress.skin.xml + openmw_quickkeys_menu.layout + openmw_quickkeys_menu_assign.layout + openmw_recharge_dialog.layout + openmw_repair.layout openmw_resources.xml + openmw_savegame_dialog.layout + openmw_screen_fader.layout + openmw_screen_fader_hit.layout openmw_scroll.layout openmw_scroll.skin.xml - openmw_settings_window.layout openmw_settings.xml + openmw_settings_window.layout + openmw_settings_window_vr.layout + openmw_spell_buying_window.layout openmw_spell_window.layout + openmw_spell_window_vr.layout + openmw_spellcreation_dialog.layout openmw_stats_window.layout - openmw_text_input.layout + openmw_stats_window_vr.layout openmw_text.skin.xml + openmw_text_input.layout openmw_tooltips.layout + openmw_tooltips_vr.layout openmw_trade_window.layout - openmw_spell_buying_window.layout - openmw_windows.skin.xml - openmw_quickkeys_menu.layout - openmw_quickkeys_menu_assign.layout - openmw_itemselection_dialog.layout - openmw_magicselection_dialog.layout - openmw_spell_buying_window.layout - openmw_loading_screen.layout - openmw_levelup_dialog.layout - openmw_wait_dialog.layout - openmw_wait_dialog_progressbar.layout - openmw_spellcreation_dialog.layout - openmw_edit_effect.layout - openmw_enchanting_dialog.layout + openmw_trade_window_vr.layout openmw_trainingwindow.layout openmw_travel_window.layout - openmw_persuasion_dialog.layout - openmw_merchantrepair.layout - openmw_repair.layout - openmw_companion_window.layout - openmw_savegame_dialog.layout - openmw_recharge_dialog.layout - openmw_screen_fader.layout - openmw_screen_fader_hit.layout - openmw_edit_note.layout - openmw_debug_window.layout - openmw_debug_window.skin.xml - openmw_jail_screen.layout + openmw_vr_listbox.layout + openmw_vr_virtual_keyboard.layout + openmw_wait_dialog.layout + openmw_wait_dialog_progressbar.layout + openmw_windows.skin.xml DejaVuLGCSansMono.ttf ../launcher/images/openmw.png OpenMWResourcePlugin.xml diff --git a/files/mygui/core_vr.xml b/files/mygui/core_vr.xml new file mode 100644 index 000000000..e4bd6984f --- /dev/null +++ b/files/mygui/core_vr.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/files/mygui/openmw_alchemy_window.layout b/files/mygui/openmw_alchemy_window.layout index 8e1082952..aa59b9999 100644 --- a/files/mygui/openmw_alchemy_window.layout +++ b/files/mygui/openmw_alchemy_window.layout @@ -83,6 +83,12 @@ + + + + + + diff --git a/files/mygui/openmw_container_window_vr.layout b/files/mygui/openmw_container_window_vr.layout new file mode 100644 index 000000000..fb1fda805 --- /dev/null +++ b/files/mygui/openmw_container_window_vr.layout @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_dialogue_window_vr.layout b/files/mygui/openmw_dialogue_window_vr.layout new file mode 100644 index 000000000..d165dc7b6 --- /dev/null +++ b/files/mygui/openmw_dialogue_window_vr.layout @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_hud_vr.layout b/files/mygui/openmw_hud_vr.layout new file mode 100644 index 000000000..f3a2bcb1d --- /dev/null +++ b/files/mygui/openmw_hud_vr.layout @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_inventory_window_vr.layout b/files/mygui/openmw_inventory_window_vr.layout new file mode 100644 index 000000000..1b5deee83 --- /dev/null +++ b/files/mygui/openmw_inventory_window_vr.layout @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_layers_vr.xml b/files/mygui/openmw_layers_vr.xml new file mode 100644 index 000000000..a09f82c95 --- /dev/null +++ b/files/mygui/openmw_layers_vr.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/files/mygui/openmw_map_window_vr.layout b/files/mygui/openmw_map_window_vr.layout new file mode 100644 index 000000000..e7f3376e5 --- /dev/null +++ b/files/mygui/openmw_map_window_vr.layout @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_savegame_dialog.layout b/files/mygui/openmw_savegame_dialog.layout index 2885deadb..3eec24b95 100644 --- a/files/mygui/openmw_savegame_dialog.layout +++ b/files/mygui/openmw_savegame_dialog.layout @@ -21,6 +21,11 @@ + + + + + diff --git a/files/mygui/openmw_settings_window_vr.layout b/files/mygui/openmw_settings_window_vr.layout new file mode 100644 index 000000000..10be64201 --- /dev/null +++ b/files/mygui/openmw_settings_window_vr.layout @@ -0,0 +1,731 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_spell_window_vr.layout b/files/mygui/openmw_spell_window_vr.layout new file mode 100644 index 000000000..092ac70ab --- /dev/null +++ b/files/mygui/openmw_spell_window_vr.layout @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_stats_window_vr.layout b/files/mygui/openmw_stats_window_vr.layout new file mode 100644 index 000000000..83ed9b0c9 --- /dev/null +++ b/files/mygui/openmw_stats_window_vr.layout @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_tooltips_vr.layout b/files/mygui/openmw_tooltips_vr.layout new file mode 100644 index 000000000..3612d0e43 --- /dev/null +++ b/files/mygui/openmw_tooltips_vr.layout @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_trade_window_vr.layout b/files/mygui/openmw_trade_window_vr.layout new file mode 100644 index 000000000..2947dba29 --- /dev/null +++ b/files/mygui/openmw_trade_window_vr.layout @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_vr_listbox.layout b/files/mygui/openmw_vr_listbox.layout new file mode 100644 index 000000000..229a5f5eb --- /dev/null +++ b/files/mygui/openmw_vr_listbox.layout @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/files/mygui/openmw_vr_metamenu.layout b/files/mygui/openmw_vr_metamenu.layout new file mode 100644 index 000000000..8b25dace3 --- /dev/null +++ b/files/mygui/openmw_vr_metamenu.layout @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/files/mygui/openmw_vr_virtual_keyboard.layout b/files/mygui/openmw_vr_virtual_keyboard.layout new file mode 100644 index 000000000..059a2f67f --- /dev/null +++ b/files/mygui/openmw_vr_virtual_keyboard.layout @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/files/settings-default.cfg b/files/settings-default.cfg index 88a6b4812..e33780f9f 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -1093,3 +1093,94 @@ stomp mode = 2 # 1 - Reduced levels. # 0 - Gentle levels. stomp intensity = 1 + +[Stereo] + +# Enable/disable stereo view. This setting is ignored in VR. +stereo enabled = false + +# Method used to render stereo if enabled +# Must be one of the following: BruteForce, GeometryShader +# BruteForce: Generates stereo using two cameras and two cull/render passes. Choose this if your game is GPU-bound. +# GeometryShader: Generates stereo in a single pass using automatically generated geometry shaders. May break custom shaders. Choose this if your game is CPU-bound. +stereo method = BruteForce + +# May accelerate the BruteForce method when shadows are enabled +shared shadow maps = true + +[VR] +# Should match your real height in the format meters.centimeters. This is used to scale your position within the vr stage to better match your character. +real height = 1.85 + +# If enabled, the game window will show your VR view. If not, it will show nothing. +mirror texture = true + +# If mirror texture is enabled, what eye to pick. Must be 'left', 'right', or 'both'. +mirror texture eye = right + +# If true, draw left eye on the right and vice versa to allow cross-eyed view. +flip mirror texture order = true + +# Resolution of each eye. 'auto' or 'recommended' uses the resolution recommended by openxr. 'max' uses the greater available resolution. If a resolution greater than the maximum reported by openxr is selected, the max recommended by openxr is used instead. The recommended and maximum resolutions will be output in openmw.log during startup. +left eye resolution x = auto +left eye resolution y = auto +right eye resolution x = auto +right eye resolution y = auto + +# Determines how quickly you have to move your hand minimum, in meters/second, to perform an attack +realistic combat minimum swing velocity = 1.0 + +# Determines how quickly you have to move your hand to achieve an attack of maximum strength. +realistic combat maximum swing velocity = 4.0 + +# Enables controller vibrations when you hit or are hit. +haptics enabled = true + +# If enabled, movement direction is taken from the left hand tracker, instead of the head tracker. +hand directed movement = false + +# Position of the hud over the left hand +# Valid options are: top, wrist +left hand hud position = wrist + +# If true, OpenMW-VR will try to use DirectX swapchains instead of OpenGL swapchains. They are HW bridged to OpenGL using the WGL_NV_DX_interop2 extension. +# As the general quality of OpenXR DirectX runtimes is better than OpenGL runtimes, i default this to true. +Prefer DirectX swapchains = true + +# If true, OpenMW-VR will use sRGB textures in its swapchains if available. Needed for some headsets that don't play nice without sRGB. +Prefer sRGB swapchains = false + +# If true, OpenMW-VR will enable seated play. +seated play = false + +[VR Debug] +# If true, OpenMW-VR will enable gamma postprocessing +gamma postprocessing = true + +# Openmw will sync with openxr at the beginning of this phase in the rendering pipeline. From early to late in the pipeline the options are update, cull, draw, and swap in that order. If you experience visual glitches such as frames jittering across your vision, try changing this to an earlier phase. +openxr sync phase = draw + +# Log all calls to openxr, not just ones that fail. Useful for debugging. +log all openxr calls = false + +# If false, openmw will quit with an exception if an openxr call fails for any reason +continue on errors = true + +# If true, enable openxr debug functionality via the XR_EXT_debug_utils extension +enable XR_EXT_debug_utils = false + +# If true, enable composition layer depth functionality via the XR_KHR_composition_layer_depth extension +# This defaults to false because either my implementation is borked or some runtimes' are. +# It'll stay in settings jail until i figure it out. +enable XR_KHR_composition_layer_depth = false + +# Enable/disable openxr debug message levels +XR_EXT_debug_utils message level verbose = false +XR_EXT_debug_utils message level info = true +XR_EXT_debug_utils message level warning = true +XR_EXT_debug_utils message level error = true +# Enable/disable openxr debug message types +XR_EXT_debug_utils message type general = true +XR_EXT_debug_utils message type validation = true +XR_EXT_debug_utils message type performance = true +XR_EXT_debug_utils message type conformance = true \ No newline at end of file diff --git a/files/settings-overrides-vr.cfg b/files/settings-overrides-vr.cfg new file mode 100644 index 000000000..2fce7e8bc --- /dev/null +++ b/files/settings-overrides-vr.cfg @@ -0,0 +1,55 @@ +# WARNING: This is a special config file that should not be edited by the user. +# Any settings listed in this file will be absolute and not modifiable by user settings. +# This is to prevent the use of settings that are incompatible with VR. +# Ignoring this and removing/editing lines in this file will either have no effect or break your game. + +[Camera] +# Automatically enable preview mode when player doesn't move. +preview if stand still = false + +# Enables head bobbing in first person mode +head bobbing = false + +[GUI] +# Scales GUI window and widget size. (<1.0 is smaller, >1.0 is larger). +scaling factor = 1.0 + +# Stretch menus, load screens, etc. to the window aspect ratio. +stretch menu background = false + +# Red flash visually showing player damage. +hit fader = false + +# Werewolf overlay border around screen or window. +werewolf overlay = false + +# Controls whether Arrow keys, Movement keys, Tab/Shift-Tab and Spacebar/Enter/Activate may be used to navigate GUI buttons. +keyboard navigation = true + +[HUD] + +# Displays the crosshair or reticle when not in GUI mode. +crosshair = false + +[Game] +# Always use the best mode of attack: e.g. chop, slash or thrust. +best attack = false + +[Video] +# OpenMW takes complete control of the screen. +fullscreen = false + +# Enable vertical syncing to reduce tearing defects. +vsync = false + +# Maximum frames per second. 0.0 is unlimited, or >0.0 to limit. +framerate limit = 0 + +# Type of screenshot to take (regular, cylindrical, spherical or planet), optionally followed by +# screenshot width, height and cubemap resolution in pixels. (e.g. spherical 1600 1000 1200) +screenshot type = regular + +[Stereo] + +# Enable/disable stereo view. +stereo enabled = true \ No newline at end of file diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index c4a863776..8b0bc10b1 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -31,6 +31,7 @@ set(SHADER_FILES nv_default_fragment.glsl nv_nolighting_vertex.glsl nv_nolighting_fragment.glsl + stereo_geometry.glsl ) copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") diff --git a/files/shaders/stereo_geometry.glsl b/files/shaders/stereo_geometry.glsl new file mode 100644 index 000000000..39ad84e5d --- /dev/null +++ b/files/shaders/stereo_geometry.glsl @@ -0,0 +1,58 @@ +#version 150 compatibility +#extension GL_ARB_viewport_array : require +//#ifdef GL_ARB_gpu_shader5 // Ref: AnyOldName3: This slightly faster path is broken on Vega 56 +#if 0 + #extension GL_ARB_gpu_shader5 : enable + #define ENABLE_GL_ARB_gpu_shader5 +#endif + +#ifdef ENABLE_GL_ARB_gpu_shader5 + layout (triangles, invocations = 2) in; + layout (triangle_strip, max_vertices = 3) out; +#else + layout (triangles) in; + layout (triangle_strip, max_vertices = 6) out; +#endif + +// Geometry Shader Inputs +@INPUTS + +// Geometry Shader Outputs +@OUTPUTS + +// Stereo matrices +uniform mat4 stereoViewMatrices[2]; +uniform mat4 stereoViewProjections[2]; + +void perVertex(int vertex, int viewport) +{ + gl_ViewportIndex = viewport; + // Re-project + gl_Position = stereoViewProjections[viewport] * vec4(vertex_passViewPos[vertex],1); + vec4 viewPos = stereoViewMatrices[viewport] * vec4(vertex_passViewPos[vertex],1); + gl_ClipVertex = vec4(viewPos.xyz,1); + + // Input -> output +@FORWARDING + + EmitVertex(); +} + +void perViewport(int viewport) +{ + for(int vertex = 0; vertex < gl_in.length(); vertex++) + { + perVertex(vertex, viewport); + } + EndPrimitive(); +} + +void main() { +#ifdef ENABLE_GL_ARB_gpu_shader5 + int viewport = gl_InvocationID; +#else + for(int viewport = 0; viewport < 2; viewport++) +#endif + perViewport(viewport); + +} \ No newline at end of file diff --git a/files/shaders/terrain_vertex.glsl b/files/shaders/terrain_vertex.glsl index 638d6cca0..570628be0 100644 --- a/files/shaders/terrain_vertex.glsl +++ b/files/shaders/terrain_vertex.glsl @@ -1,4 +1,4 @@ -#version 120 +#version 130 #if @useUBO #extension GL_ARB_uniform_buffer_object : require diff --git a/files/shaders/water_vertex.glsl b/files/shaders/water_vertex.glsl index 02a395f95..c099e4210 100644 --- a/files/shaders/water_vertex.glsl +++ b/files/shaders/water_vertex.glsl @@ -3,12 +3,15 @@ varying vec3 screenCoordsPassthrough; varying vec4 position; varying float linearDepth; +varying vec3 passViewPos; #include "shadows_vertex.glsl" void main(void) { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; + vec4 viewPos = gl_ModelViewMatrix * gl_Vertex; + passViewPos = viewPos.xyz; mat4 scalemat = mat4(0.5, 0.0, 0.0, 0.0, 0.0, -0.5, 0.0, 0.0, @@ -22,5 +25,5 @@ void main(void) linearDepth = gl_Position.z; - setupShadowCoords(gl_ModelViewMatrix * gl_Vertex, normalize((gl_NormalMatrix * gl_Normal).xyz)); + setupShadowCoords(viewPos, normalize((gl_NormalMatrix * gl_Normal).xyz)); } diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 8abe8f59a..62ed7a601 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -1006,6 +1006,16 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C + + + + <html><head/><body><p>Enable zooming on local and global maps.</p></body></html> + + + Can zoom on maps + + + @@ -1121,7 +1131,7 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C - + Screenshot Format @@ -1149,6 +1159,13 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C + + + + Notify on saved screenshot + + + @@ -1276,10 +1293,200 @@ True: In non-combat mode camera is positioned behind the character's shoulder. C + + + VR + + + + + + + + <html><head/><body><p>The velocity at which the minimum strength of attack is achieved when swinging your motion controllers. In meters/second. Swinging slower than this does not initiate an attack.</p></body></html> + + + Realistic melee combat minimum swing speed + + + + + + + true + + + 2 + + + 0 + + + 100 + + + 0.00 + + + 0.100000000000000 + + + + + + + <html><head/><body><p>The velocity at which max strength of attack is achieved when swinging your motion controllers. In meters/second</p></body></html> + + + Realistic melee combat minimum swing speed + + + + + + + true + + + 2 + + + 0 + + + 100 + + + 0.00 + + + 0.100000000000000 + + + + + + + <html><head/><body><p>Your real height. In meters.</p></body></html> + + + Real height (Meters) + + + + + + + true + + + 2 + + + 0 + + + 100 + + + 0.00 + + + 0.010000000000000 + + + + + + + <html><head/><body><p>Makes OpenMW VR use sRGB format for its swapchain textures if available. Needed for some headsets that don't play nice without sRGB. Enable this if the game seems much brighter in your headset than what's shown on the mirror texture.</p></body></html> + + + Prefer sRGB swapchains. + + + + + + + <html><head/><body><p>Makes OpenMW VR map shadows for both eyes in one pass, instead of once per eye. This is a significant performance benefit, and should only be disabled for debugging or masochism purposes.</p></body></html> + + + Use shared shadow maps + + + + + + + <html><head/><body><p>Makes OpenMW VR render stereo using geometry shaders instead of two render passes. Players with a CPU bound will benefit from this, but players with a gpu bound will not. E.g. with a high cell view distance (distant lands) this will degrade your performance instead. NOTE: This feature is experimental, broken on AMD GPUs, and untested on Intel.</p></body></html> + + + !!Experimental!!: Use geometry shaders. + + + + + + + <html><head/><body><p>Makes OpenMW VR use DirectX swapchains even if OpenGL swapchains are available. Note that OpenMW will always fall back to DirectX swapchains if OpenGL swapchains are not available. This option exists to debug/work around runtimes with bad OpenGL implementations.</p></body></html> + + + Debug: Prefer DirectX Swapchains + + + + + + + <html><head/><body><p>Makes OpenMW VR enable and use the XR_EXT_debug_utils extension, which may help produce more detailed debug information.</p></body></html> + + + Debug: Enable extension XR_EXT_debug_utils + + + + + + + <html><head/><body><p>Makes OpenMW VR log every single OpenXR call. This degrades performance and creates a massive log. You probably want this disabled.</p></body></html> + + + Debug: Log all OpenXR calls + + + + + + + <html><head/><body><p>Makes OpenMW VR ignore unsuccessful calls to OpenXR. You probably want this enabled.</p></body></html> + + + Debug: Continue on errors + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - + \ No newline at end of file diff --git a/files/xrcontrollersuggestions.xml b/files/xrcontrollersuggestions.xml new file mode 100644 index 000000000..fdef7eaa8 --- /dev/null +++ b/files/xrcontrollersuggestions.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file