From db4525abf9dd86f14e94546d3f7d91ed57772053 Mon Sep 17 00:00:00 2001 From: James Hogan <james@albanarts.com> Date: Sat, 17 Jul 2021 23:19:14 +0100 Subject: [PATCH] VR: Implement minimal VR support Implement support for virtual reality headsets via version 0.3 of the osgXR[1] library I've been working on which uses OpenXR. Add a new VRManager class based on osgXR::Manager to implement its callbacks. When osgXR needs a new view created, we build a new camera in the default camera group, and notify osgXR of each new "scene" typed render pass. It also hooks into the CameraInfo's new compositor reload callback to ensure osgXR is updated when the compositors are reloaded. VR settings are controlled by properties, and new --enable-vr / --disable-vr options are implemented to enable/disable VR at start. This is enough to get basic VR for looking around the cockpit, but more work will be required to support a desktop mirror of VR view, VR splash screen, VR GUI, controller interaction, and correct positional sound. [1] https://github.com/amalon/osgXR --- scripts/completion/fg-completion.zsh | 2 + src/Include/config_cmake.h.in | 1 + src/Main/options.cxx | 4 + src/Viewer/CMakeLists.txt | 7 + src/Viewer/VRManager.cxx | 221 +++++++++++++++++++++++++++ src/Viewer/VRManager.hxx | 158 +++++++++++++++++++ src/Viewer/fg_os_osgviewer.cxx | 13 ++ 7 files changed, 406 insertions(+) create mode 100644 src/Viewer/VRManager.cxx create mode 100644 src/Viewer/VRManager.hxx diff --git a/scripts/completion/fg-completion.zsh b/scripts/completion/fg-completion.zsh index 3586fedf4..7fd0dbea7 100755 --- a/scripts/completion/fg-completion.zsh +++ b/scripts/completion/fg-completion.zsh @@ -60,6 +60,8 @@ _fgfs_options=( '--enable-skyblend[Enable sky blending]' \ '--disable-textures[Disable textures]' \ '--enable-textures[Enable textures]' \ + '--disable-vr[Disable VR]' \ + '--enable-vr[Enable VR]' \ '--disable-wireframe[Disable wireframe drawing mode]' \ '--enable-wireframe[Enable wireframe drawing mode]' \ '--notrim[Do NOT attempt to trim the model (only with fdm=jsbsim)]' \ diff --git a/src/Include/config_cmake.h.in b/src/Include/config_cmake.h.in index db44de3b1..b7c67d055 100644 --- a/src/Include/config_cmake.h.in +++ b/src/Include/config_cmake.h.in @@ -27,6 +27,7 @@ #define FLIGHTGEAR_VERSION "@FLIGHTGEAR_VERSION@" #define FLIGHTGEAR_MAJOR_VERSION @FG_VERSION_MAJOR@ #define FLIGHTGEAR_MINOR_VERSION @FG_VERSION_MINOR@ +#define FLIGHTGEAR_PATCH_VERSION @FG_VERSION_PATCH@ #cmakedefine ENABLE_UIUC_MODEL #cmakedefine ENABLE_LARCSIM diff --git a/src/Main/options.cxx b/src/Main/options.cxx index f0f9c43e3..7bb02f76b 100644 --- a/src/Main/options.cxx +++ b/src/Main/options.cxx @@ -1900,6 +1900,10 @@ struct OptionDesc { {"disable-wireframe", false, OPTION_BOOL, "/sim/rendering/wireframe", false, "", 0 }, {"enable-wireframe", false, OPTION_BOOL, "/sim/rendering/wireframe", true, "", 0 }, {"materials-file", true, OPTION_STRING, "/sim/rendering/materials-file", false, "", 0 }, +#ifdef ENABLE_OSGXR + {"disable-vr", false, OPTION_BOOL, "/sim/vr/enabled", false, "", 0 }, + {"enable-vr", false, OPTION_BOOL, "/sim/vr/enabled", true, "", 0 }, +#endif {"disable-terrasync", false, OPTION_BOOL, "/sim/terrasync/enabled", false, "", 0 }, {"enable-terrasync", false, OPTION_BOOL, "/sim/terrasync/enabled", true, "", 0 }, {"terrasync-dir", true, OPTION_IGNORE, "", false, "", 0 }, diff --git a/src/Viewer/CMakeLists.txt b/src/Viewer/CMakeLists.txt index f65fee469..6646fde85 100644 --- a/src/Viewer/CMakeLists.txt +++ b/src/Viewer/CMakeLists.txt @@ -28,8 +28,15 @@ set(HEADERS viewmgr.hxx sview.hxx GraphicsPresets.hxx + VRManager.hxx ) +if (ENABLE_OSGXR) + list(APPEND SOURCES + VRManager.cxx + ) +endif() + if (YES) list(APPEND HEADERS PUICamera.hxx) list(APPEND SOURCES PUICamera.cxx) diff --git a/src/Viewer/VRManager.cxx b/src/Viewer/VRManager.cxx new file mode 100644 index 000000000..8417df65c --- /dev/null +++ b/src/Viewer/VRManager.cxx @@ -0,0 +1,221 @@ +// Copyright (C) 2021 James Hogan +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#include "VRManager.hxx" +#include "WindowBuilder.hxx" +#include "renderer.hxx" + +#include <osgXR/Settings> + +#include <simgear/scene/viewer/CompositorPass.hxx> + +#include <Main/fg_props.hxx> +#include <Main/globals.hxx> + +namespace flightgear +{ + +VRManager::VRManager() : + _reloadCompositorCallback(new ReloadCompositorCallback(this)), + _propXrLayersValidation("/sim/vr/openxr/layers/validation"), + _propXrExtensionsDepthInfo("/sim/vr/openxr/extensions/depth-info"), + _propXrRuntimeName("/sim/vr/openxr/runtime/name"), + _propXrSystemName("/sim/vr/openxr/system/name"), + _propStateString("/sim/vr/state-string"), + _propPresent("/sim/vr/present"), + _propRunning("/sim/vr/running"), + _propEnabled("/sim/vr/enabled"), + _propDepthInfo("/sim/vr/depth-info"), + _propValidationLayer("/sim/vr/validation-layer"), + _propMode("/sim/vr/mode"), + _propSwapchainMode("/sim/vr/swapchain-mode"), + _listenerEnabled(this, &osgXR::Manager::setEnabled), + _listenerDepthInfo(this, &VRManager::setDepthInfo), + _listenerValidationLayer(this, &VRManager::setValidationLayer), + _listenerMode(this, &VRManager::setVRMode), + _listenerSwapchainMode(this, &VRManager::setSwapchainMode) +{ + uint32_t fgVersion = (FLIGHTGEAR_MAJOR_VERSION << 16 | + FLIGHTGEAR_MINOR_VERSION << 8 | + FLIGHTGEAR_PATCH_VERSION); + _settings->setApp("FlightGear", fgVersion); + _settings->preferEnvBlendMode(osgXR::Settings::OPAQUE); + + // Hook into viewer, but don't enable VR just yet + osgViewer::View *view = globals->get_renderer()->getView(); + if (view) { + setViewer(globals->get_renderer()->getViewerBase()); + view->apply(this); + } + + syncReadOnlyProperties(); + + _propEnabled.node(true)->addChangeListener(&_listenerEnabled, true); + _propDepthInfo.node(true)->addChangeListener(&_listenerDepthInfo, true); + _propValidationLayer.node(true)->addChangeListener(&_listenerValidationLayer, true); + _propMode.node(true)->addChangeListener(&_listenerMode, true); + _propSwapchainMode.node(true)->addChangeListener(&_listenerSwapchainMode, true); +} + +VRManager *VRManager::instance() +{ + static osg::ref_ptr<VRManager> single = new VRManager; + return single; +} + +void VRManager::syncProperties() +{ + // If the state has changed, properties may need synchronising + if (checkAndResetStateChanged()) { + syncReadOnlyProperties(); + syncSettingProperties(); + } +} + +void VRManager::syncReadOnlyProperties() +{ + _propXrLayersValidation = hasValidationLayer(); + _propXrExtensionsDepthInfo = hasDepthInfoExtension(); + _propXrRuntimeName = getRuntimeName(); + _propXrSystemName = getSystemName(); + + _propStateString = getStateString(); + _propPresent = getPresent(); + _propRunning = isRunning(); +} + +void VRManager::syncSettingProperties() +{ + bool enabled = getEnabled(); + if (_propEnabled != enabled) + _propEnabled = enabled; +} + +void VRManager::setValidationLayer(bool validationLayer) +{ + _settings->setValidationLayer(validationLayer); + syncSettings(); +} + +void VRManager::setDepthInfo(bool depthInfo) +{ + _settings->setDepthInfo(depthInfo); + syncSettings(); +} + +void VRManager::setVRMode(const char * mode) +{ + osgXR::Settings::VRMode vrMode = osgXR::Settings::VRMODE_AUTOMATIC; + + if (strcmp(mode, "AUTOMATIC") == 0) { + vrMode = osgXR::Settings::VRMODE_AUTOMATIC; + } else if (strcmp(mode, "SLAVE_CAMERAS") == 0) { + vrMode = osgXR::Settings::VRMODE_SLAVE_CAMERAS; + } else if (strcmp(mode, "SCENE_VIEW") == 0) { + vrMode = osgXR::Settings::VRMODE_SCENE_VIEW; + } + + _settings->setVRMode(vrMode); + syncSettings(); +} + +void VRManager::setSwapchainMode(const char * mode) +{ + osgXR::Settings::SwapchainMode swapchainMode = osgXR::Settings::SWAPCHAIN_AUTOMATIC; + + if (strcmp(mode, "AUTOMATIC") == 0) { + swapchainMode = osgXR::Settings::SWAPCHAIN_AUTOMATIC; + } else if (strcmp(mode,"MULTIPLE") == 0) { + swapchainMode = osgXR::Settings::SWAPCHAIN_MULTIPLE; + } else if (strcmp(mode,"SINGLE") == 0) { + swapchainMode = osgXR::Settings::SWAPCHAIN_SINGLE; + } + + _settings->setSwapchainMode(swapchainMode); + syncSettings(); +} + +void VRManager::update() +{ + osgXR::Manager::update(); + syncProperties(); +} + +void VRManager::doCreateView(osgXR::View *xrView) +{ + // Restarted in osgXR::Manager::update() + _viewer->stopThreading(); + + // Construct a property tree for the camera + SGPropertyNode_ptr camNode = new SGPropertyNode; + WindowBuilder *windowBuilder = WindowBuilder::getWindowBuilder(); + setValue(camNode->getNode("window/name", true), + windowBuilder->getDefaultWindowName()); + + // Build a camera + CameraGroup *cgroup = CameraGroup::getDefault(); + CameraInfo *info = cgroup->buildCamera(camNode); + + // Notify osgXR about the new compositor's scene slave cameras + if (info) { + _camInfos[xrView] = info; + _xrViews[info] = xrView; + info->reloadCompositorCallback = _reloadCompositorCallback; + + postReloadCompositor(cgroup, info); + } +} + +void VRManager::doDestroyView(osgXR::View *xrView) +{ + // Restarted in osgXR::Manager::update() + _viewer->stopThreading(); + + CameraGroup *cgroup = CameraGroup::getDefault(); + auto it = _camInfos.find(xrView); + if (it != _camInfos.end()) { + osg::ref_ptr<CameraInfo> info = (*it).second; + _camInfos.erase(it); + + auto it2 = _xrViews.find(info.get()); + if (it2 != _xrViews.end()) + _xrViews.erase(it2); + + cgroup->removeCamera(info.get()); + } +} + +void VRManager::preReloadCompositor(CameraGroup *cgroup, CameraInfo *info) +{ + osgXR::View *xrView = _xrViews[info]; + + auto& passes = info->compositor->getPassList(); + for (auto& pass: passes) + if (pass->type == "scene") + xrView->removeSlave(pass->camera); +} + +void VRManager::postReloadCompositor(CameraGroup *cgroup, CameraInfo *info) +{ + osgXR::View *xrView = _xrViews[info]; + + auto& passes = info->compositor->getPassList(); + for (auto& pass: passes) + if (pass->type == "scene") + xrView->addSlave(pass->camera); +} + +} diff --git a/src/Viewer/VRManager.hxx b/src/Viewer/VRManager.hxx new file mode 100644 index 000000000..db97f682d --- /dev/null +++ b/src/Viewer/VRManager.hxx @@ -0,0 +1,158 @@ +// Copyright (C) 2021 James Hogan +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +#ifndef VRMANAGER_HXX +#define VRMANAGER_HXX 1 + +#include <config.h> + +#ifdef ENABLE_OSGXR + +#include <osg/ref_ptr> +#include <osg/observer_ptr> + +#include <osgXR/Manager> + +#include <simgear/props/propertyObject.hxx> +#include <simgear/scene/viewer/CompositorPass.hxx> + +#include "CameraGroup.hxx" + +#include <map> + +namespace flightgear +{ + +class VRManager : public osgXR::Manager +{ + public: + + class ReloadCompositorCallback : public CameraInfo::ReloadCompositorCallback + { + public: + + ReloadCompositorCallback(VRManager *manager) : + _manager(manager) + { + }; + + virtual void preReloadCompositor(CameraGroup *cgroup, CameraInfo *info) + { + _manager->preReloadCompositor(cgroup, info); + } + + virtual void postReloadCompositor(CameraGroup *cgroup, CameraInfo *info) + { + _manager->postReloadCompositor(cgroup, info); + } + + protected: + + osg::observer_ptr<VRManager> _manager; + }; + + VRManager(); + + static VRManager *instance(); + + void syncProperties(); + void syncReadOnlyProperties(); + void syncSettingProperties(); + + // Settings + + void setValidationLayer(bool validationLayer); + void setDepthInfo(bool depthInfo); + + void setVRMode(const char * mode); + void setSwapchainMode(const char * mode); + + // osgXR::Manager overrides + + void update() override; + + void doCreateView(osgXR::View *xrView) override; + void doDestroyView(osgXR::View *xrView) override; + + void preReloadCompositor(CameraGroup *cgroup, CameraInfo *info); + void postReloadCompositor(CameraGroup *cgroup, CameraInfo *info); + + protected: + + typedef std::map<osgXR::View *, osg::ref_ptr<CameraInfo>> XRViewToCamInfo; + XRViewToCamInfo _camInfos; + + typedef std::map<CameraInfo *, osg::ref_ptr<osgXR::View>> CamInfoToXRView; + CamInfoToXRView _xrViews; + + osg::ref_ptr<ReloadCompositorCallback> _reloadCompositorCallback; + + // Properties + + SGPropObjBool _propXrLayersValidation; + SGPropObjBool _propXrExtensionsDepthInfo; + SGPropObjString _propXrRuntimeName; + SGPropObjString _propXrSystemName; + + SGPropObjString _propStateString; + SGPropObjBool _propPresent; + SGPropObjBool _propRunning; + + SGPropObjBool _propEnabled; + SGPropObjBool _propDepthInfo; + SGPropObjBool _propValidationLayer; + SGPropObjString _propMode; + SGPropObjString _propSwapchainMode; + + // Property listeners + + template <typename T> + class Listener : public SGPropertyChangeListener + { + public: + typedef void (VRManager::*SetterFn)(T v); + + Listener(VRManager *manager, SetterFn setter) : + _manager(manager), + _setter(setter) + { + } + + void valueChanged(SGPropertyNode *node) override + { + (_manager->*_setter)(node->template getValue<T>()); + } + + protected: + + VRManager *_manager; + SetterFn _setter; + }; + typedef Listener<bool> ListenerBool; + typedef Listener<const char *> ListenerString; + + ListenerBool _listenerEnabled; + ListenerBool _listenerDepthInfo; + ListenerBool _listenerValidationLayer; + ListenerString _listenerMode; + ListenerString _listenerSwapchainMode; +}; + +} + +#endif // ENABLE_OSGXR + +#endif diff --git a/src/Viewer/fg_os_osgviewer.cxx b/src/Viewer/fg_os_osgviewer.cxx index 600fbcf73..dae44a64d 100644 --- a/src/Viewer/fg_os_osgviewer.cxx +++ b/src/Viewer/fg_os_osgviewer.cxx @@ -52,6 +52,7 @@ #include "renderer.hxx" #include "CameraGroup.hxx" #include "FGEventHandler.hxx" +#include "VRManager.hxx" #include "WindowBuilder.hxx" #include "WindowSystemAdapter.hxx" #include <Main/sentryIntegration.hxx> @@ -328,6 +329,12 @@ void fgOSResetProperties() fgTie("/sim/rendering/osg-displaysettings/double-buffer", displaySettings, &DisplaySettings::getDoubleBuffer, &DisplaySettings::setDoubleBuffer ); fgTie("/sim/rendering/osg-displaysettings/depth-buffer", displaySettings, &DisplaySettings::getDepthBuffer, &DisplaySettings::setDepthBuffer ); fgTie("/sim/rendering/osg-displaysettings/rgb", displaySettings, &DisplaySettings::getRGB, &DisplaySettings::setRGB ); + +#ifdef ENABLE_OSGXR + fgSetBool("/sim/vr/built", true); +#else + fgSetBool("/sim/vr/built", false); +#endif } @@ -389,6 +396,9 @@ int fgOSMainLoop() } } globals->get_renderer()->update(); +#ifdef ENABLE_OSGXR + VRManager::instance()->update(); +#endif viewer_base->frame( globals->get_sim_time_sec() ); } @@ -437,6 +447,9 @@ void fgOSCloseWindow() viewer_base->stopThreading(); } } +#ifdef ENABLE_OSGXR + VRManager::instance()->destroyAndWait(); +#endif FGScenery::resetPagerSingleton(); flightgear::addSentryBreadcrumb("fgOSCloseWindow, clearing camera group", "info"); flightgear::CameraGroup::setDefault(NULL);