diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index 3abc01d2e..d45b9daaa 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" @@ -106,7 +107,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 } @@ -134,7 +135,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); @@ -246,7 +247,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()); } void SceneWidget::setLighting(Lighting *lighting) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index ead9d4f86..f7b65183f 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -24,12 +24,16 @@ #include #include +#include + #include #include #include #include +#include + #include #include @@ -376,6 +380,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) @@ -409,6 +416,8 @@ OMW::Engine::Engine(Files::ConfigurationManager& configurationManager) OMW::Engine::~Engine() { + mStereoView = nullptr; + mEnvironment.cleanup(); delete mScriptContext; @@ -761,12 +770,32 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) window->playVideo(logo, true); } + // VR mode will override this setting by setting mStereoOverride. + mStereoEnabled = mStereoOverride || Settings::Manager::getBool("stereo enabled", "Stereo"); + + // geometry shader must be enabled before the RenderingManager sets up any shaders + // therefore this part is separate from the rest of stereo setup. + if (mStereoEnabled) + { + mResourceSystem->getSceneManager()->getShaderManager().setStereoGeometryShaderEnabled(Misc::getStereoTechnique() == Misc::StereoView::Technique::GeometryShader_IndexedViewports); + } + // 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(); + // Set up 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; + auto geometryShaderMask = mViewer->getCamera()->getCullMask() & ~noShaderMask; + mStereoView = new Misc::StereoView(mViewer, Misc::getStereoTechnique(), geometryShaderMask, noShaderMask | MWRender::VisMask::Mask_Scene); + } + window->setStore(mEnvironment.getWorld()->getStore()); window->initUI(); diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 40a557202..cb526db71 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -37,6 +37,11 @@ namespace Compiler class Context; } +namespace Misc +{ + class StereoView; +} + namespace MWScript { class ScriptManager; @@ -89,6 +94,11 @@ namespace OMW osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; std::string mCellName; std::vector mContentFiles; + + bool mStereoEnabled; + bool mStereoOverride; + osg::ref_ptr mStereoView; + bool mSkipMenu; bool mUseSound; bool mCompileAll; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 4d6b47de1..1946dff5d 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include @@ -215,8 +216,9 @@ namespace MWRender resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); // Shadows and radial fog have problems with fixed-function mode - bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows"); - resourceSystem->getSceneManager()->setForceShaders(forceShaders); + //bool forceShaders = Settings::Manager::getBool("radial fog", "Shaders") || Settings::Manager::getBool("force shaders", "Shaders") || Settings::Manager::getBool("enable shadows", "Shadows"); + //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")); @@ -383,12 +385,14 @@ 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)); mUniformNear = mRootNode->getOrCreateStateSet()->getUniform("near"); mUniformFar = mRootNode->getOrCreateStateSet()->getUniform("far"); + updateProjectionMatrix(); } diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index daadeff04..52d3d3373 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -123,6 +123,8 @@ namespace MWRender osg::Uniform* mUniformNear; osg::Uniform* mUniformFar; + osg::Uniform* mUniformStereoViewOffsets; + osg::Uniform* mUniformStereoProjections; void preloadCommonAssets(); diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index b9018e0a2..60a5b0bac 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -29,6 +30,7 @@ #include #include +#include #include @@ -343,6 +345,7 @@ public: 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) @@ -596,6 +599,19 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R std::map defineMap; defineMap.insert(std::make_pair(std::string("refraction_enabled"), std::string(refraction ? "1" : "0"))); + if (mResourceSystem->getSceneManager()->getShaderManager().stereoGeometryShaderEnabled()) + { + defineMap["geometryShader"] = "1"; + if (reflection) + { + Misc::enableStereoForCamera(reflection, true); + } + if (refraction) + { + Misc::enableStereoForCamera(refraction, true); + } + } + 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)); @@ -640,9 +656,7 @@ void Water::createShaderWaterStateSet(osg::Node* node, Reflection* reflection, R shaderStateset->addUniform(mRainIntensityUniform.get()); - osg::ref_ptr program (new osg::Program); - program->addShader(vertexShader); - program->addShader(fragmentShader); + auto program = shaderMgr.getProgram(vertexShader, fragmentShader); shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); node->setStateSet(shaderStateset); diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 481ba3afe..224af6d05 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -87,7 +87,7 @@ add_component_dir (esmterrain ) add_component_dir (misc - constants utf8stream stringops resourcehelpers rng messageformatparser weakcache + gcd constants utf8stream stringops resourcehelpers rng messageformatparser weakcache stereo ) add_component_dir (debug diff --git a/components/misc/stereo.cpp b/components/misc/stereo.cpp new file mode 100644 index 000000000..b57c71b01 --- /dev/null +++ b/components/misc/stereo.cpp @@ -0,0 +1,529 @@ +#include "stereo.hpp" +#include "stringops.hpp" + +#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 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; + }; + + StereoView::StereoView(osgViewer::Viewer* viewer, Technique technique, osg::Node::NodeMask geometryShaderMask, osg::Node::NodeMask noShaderMask) + : osg::Group() + , mViewer(viewer) + , mMainCamera(mViewer->getCamera()) + , mRoot(viewer->getSceneData()->asGroup()) + , mTechnique(technique) + , mGeometryShaderMask(geometryShaderMask) + , mNoShaderMask(noShaderMask) + { + if (technique == Technique::None) + // Do nothing + return; + + SceneUtil::FindByNameVisitor findScene("Scene Root"); + mRoot->accept(findScene); + mScene = findScene.mFoundNode; + if (!mScene) + throw std::logic_error("Couldn't find scene root"); + + setName("Stereo Root"); + mRoot->setDataVariance(osg::Object::STATIC); + setDataVariance(osg::Object::STATIC); + mLeftCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + mLeftCamera->setProjectionResizePolicy(osg::Camera::FIXED); + mLeftCamera->setProjectionMatrix(osg::Matrix::identity()); + mLeftCamera->setViewMatrix(osg::Matrix::identity()); + mLeftCamera->setName("Stereo Left"); + mLeftCamera->setDataVariance(osg::Object::STATIC); + mRightCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + mRightCamera->setProjectionResizePolicy(osg::Camera::FIXED); + mRightCamera->setProjectionMatrix(osg::Matrix::identity()); + mRightCamera->setViewMatrix(osg::Matrix::identity()); + mRightCamera->setName("Stereo Right"); + mRightCamera->setDataVariance(osg::Object::STATIC); + + // Update stereo statesets/matrices, but after the main camera updates. + auto mainCameraCB = mMainCamera->getUpdateCallback(); + mMainCamera->removeUpdateCallback(mainCameraCB); + mMainCamera->addUpdateCallback(new StereoUpdateCallback(this)); + mMainCamera->addUpdateCallback(mainCameraCB); + + // Do a blank double buffering of camera statesets on update. Actual state updates are performed in StereoView::Update() + mLeftCamera->setUpdateCallback(new SceneUtil::StateSetUpdater()); + mRightCamera->setUpdateCallback(new SceneUtil::StateSetUpdater()); + + + if (mTechnique == Technique::GeometryShader_IndexedViewports) + { + setupGeometryShaderIndexedViewportTechnique(); + } + else + { + setupBruteForceTechnique(); + } + } + + void StereoView::setupBruteForceTechnique() + { + mLeftCamera->setRenderOrder(osg::Camera::NESTED_RENDER); + mLeftCamera->setClearColor(mMainCamera->getClearColor()); + mLeftCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + mLeftCamera->setCullMask(mMainCamera->getCullMask()); + mRightCamera->setRenderOrder(osg::Camera::NESTED_RENDER); + mRightCamera->setClearColor(mMainCamera->getClearColor()); + mRightCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + mRightCamera->setCullMask(mMainCamera->getCullMask()); + + // Slave cameras must have their viewports defined immediately + auto width = mMainCamera->getViewport()->width(); + auto height = mMainCamera->getViewport()->height(); + mLeftCamera->setViewport(0, 0, width / 2, height); + mRightCamera->setViewport(width / 2, 0, width / 2, height); + + mViewer->stopThreading(); + mViewer->addSlave(mLeftCamera, true); + mViewer->addSlave(mRightCamera, true); + mRightCamera->setGraphicsContext(mViewer->getCamera()->getGraphicsContext()); + mLeftCamera->setGraphicsContext(mViewer->getCamera()->getGraphicsContext()); + mViewer->getCamera()->setGraphicsContext(nullptr); + mViewer->realize(); + } + + void StereoView::setupGeometryShaderIndexedViewportTechnique() + { + mLeftCamera->setRenderOrder(osg::Camera::NESTED_RENDER); + mLeftCamera->setClearMask(GL_NONE); + mLeftCamera->setCullMask(mNoShaderMask); + mRightCamera->setRenderOrder(osg::Camera::NESTED_RENDER); + mRightCamera->setClearMask(GL_NONE); + mRightCamera->setCullMask(mNoShaderMask); + mMainCamera->setCullMask(mGeometryShaderMask); + + addChild(mStereoGeometryShaderRoot); + mStereoGeometryShaderRoot->addChild(mRoot); + addChild(mStereoBruteForceRoot); + mStereoBruteForceRoot->addChild(mLeftCamera); + mLeftCamera->addChild(mScene); // Use scene directly to avoid redundant shadow computation. + mStereoBruteForceRoot->addChild(mRightCamera); + mRightCamera->addChild(mScene); + + addCullCallback(new StereoStatesetUpdateCallback(this)); + + // Inject self as the root of the scene graph + mViewer->setSceneData(this); + } + + void StereoView::update() + { + auto viewMatrix = mViewer->getCamera()->getViewMatrix(); + auto projectionMatrix = mViewer->getCamera()->getProjectionMatrix(); + + View left{}; + View right{}; + double near = 1.f; + double far = 10000.f; + if (!cb) + { + Log(Debug::Error) << "No update view callback. Stereo rendering will not work."; + } + cb->updateView(left, right, near, far); + + 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(); + + if (mTechnique == Technique::GeometryShader_IndexedViewports) + { + // 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 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); + + // 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); + } + } + + 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) + { + this->cb = 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, double& near, double& far) + { + 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.837898076, 0.726982594 }; + right.fov = { -0.620896876, 0.767549932, -0.837898076, 0.726982594 }; + near = 1; + far = 6656; + } +} diff --git a/components/misc/stereo.hpp b/components/misc/stereo.hpp new file mode 100644 index 000000000..c721f6a1c --- /dev/null +++ b/components/misc/stereo.hpp @@ -0,0 +1,142 @@ +#ifndef MISC_STEREO_H +#define MISC_STEREO_H + +#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{ -osg::PI_2 }; + float angleRight{ osg::PI_2 }; + float angleDown{ -osg::PI_2 }; + float angleUp{ osg::PI_2 }; + + bool operator==(const FieldOfView& rhs) const; + + //! Generate a perspective matrix from this fov + osg::Matrix perspectiveMatrix(float near, float far); + }; + + //! 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 : public osg::Group + { + struct UpdateViewCallback + { + //! Called during the update traversal of every frame to source updated stereo values. + virtual void updateView(View& left, View& right, double& near, double& far) = 0; + }; + + //! Default implementation of UpdateViewCallback that just provides some hardcoded values for debugging purposes + struct DefaultUpdateViewCallback : public UpdateViewCallback + { + virtual void updateView(View& left, View& right, double& near, double& far); + }; + + 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. + }; + + //! 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 geometryShaderMask should mask in all nodes that use shaders. + //! \param noShaderMask mask in all nodes that do not use shaders and must be rendered brute force. + //! \note the masks apply only to the GeometryShader_IndexdViewports technique and can be 0 for the BruteForce technique. + StereoView(osgViewer::Viewer* viewer, Technique technique, osg::Node::NodeMask geometryShaderMask, osg::Node::NodeMask noShaderMask); + + //! 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); + + //! Callback that updates stereo configuration during the update pass + void setUpdateViewCallback(std::shared_ptr cb); + + private: + void setupBruteForceTechnique(); + void setupGeometryShaderIndexedViewportTechnique(); + + osg::ref_ptr mViewer; + osg::ref_ptr mMainCamera; + osg::ref_ptr mRoot; + osg::ref_ptr mScene; + Technique mTechnique; + + // Keeps state relevant to doing stereo via the geometry shader + osg::ref_ptr mStereoGeometryShaderRoot{ new osg::Group }; + osg::Node::NodeMask mGeometryShaderMask; + osg::Node::NodeMask mNoShaderMask; + + // 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 }; + + // Camera viewports + bool flipViewOrder{ true }; + + // Updates stereo configuration during the update pass + std::shared_ptr cb{ new DefaultUpdateViewCallback }; + }; + + //! 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/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 2630cd453..c55024261 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -380,18 +380,30 @@ namespace Resource return false; static std::vector reservedNames; - if (reservedNames.empty()) + static std::mutex reservedNamesMutex; { - const char* reserved[] = {"Head", "Neck", "Chest", "Groin", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", "Shield Bone", "Right Forearm", "Left Forearm", "Right Upper Arm", - "Left Upper Arm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Upper Leg", "Left Upper Leg", "Right Clavicle", - "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera"}; + std::lock_guard lock(reservedNamesMutex); + if (reservedNames.empty()) + { + // This keeps somehow accessing garbage so i rewrote it using safer types. + //const char* reserved[] = {"Head", "Neck", "Chest", "Groin", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", "Shield Bone", "Right Forearm", "Left Forearm", "Right Upper Arm", + // "Left Upper Arm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Upper Leg", "Left Upper Leg", "Right Clavicle", + // "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera"}; - reservedNames = std::vector(reserved, reserved + sizeof(reserved)/sizeof(reserved[0])); + //reservedNames = std::vector(reserved, reserved + sizeof(reserved)/sizeof(const char*)); - for (unsigned int i=0; i r = { "Head", "Neck", "Chest", "Groin", "Right Hand", "Left Hand", "Right Wrist", "Left Wrist", "Shield Bone", "Right Forearm", "Left Forearm", "Right Upper Arm", + "Left Upper Arm", "Right Foot", "Left Foot", "Right Ankle", "Left Ankle", "Right Knee", "Left Knee", "Right Upper Leg", "Left Upper Leg", "Right Clavicle", + "Left Clavicle", "Weapon Bone", "Tail", "Bip01", "Root Bone", "BoneOffset", "AttachLight", "Arrow", "Camera" }; + reservedNames = std::vector(r.begin(), r.end()); + for (auto& reservedName : r) + reservedNames.emplace_back(std::string("Tri ") + reservedName); + + std::sort(reservedNames.begin(), reservedNames.end(), Misc::StringUtils::ciLess); + } } std::vector::iterator it = Misc::StringUtils::partialBinarySearch(reservedNames.begin(), reservedNames.end(), name); diff --git a/components/sceneutil/mwshadowtechnique.cpp b/components/sceneutil/mwshadowtechnique.cpp index f32b2da4d..bf91d5c16 100644 --- a/components/sceneutil/mwshadowtechnique.cpp +++ b/components/sceneutil/mwshadowtechnique.cpp @@ -18,6 +18,8 @@ #include "mwshadowtechnique.hpp" +#include + #include #include #include @@ -587,6 +589,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) { diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index ad7ecd9ae..39b479f29 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -1,5 +1,7 @@ #include "sdlgraphicswindow.hpp" +#include + #include namespace SDLUtil @@ -225,6 +227,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 (auto 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 be0622d69..99e2b5ae2 100644 --- a/components/sdlutil/sdlinputwrapper.cpp +++ b/components/sdlutil/sdlinputwrapper.cpp @@ -1,4 +1,5 @@ #include "sdlinputwrapper.hpp" +#include "sdlgraphicswindow.hpp" #include #include @@ -213,11 +214,7 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr v SDL_GetWindowSize(mSDLWindow, &w, &h); int x,y; SDL_GetWindowPosition(mSDLWindow, &x,&y); - { - auto* gc = mViewer->getCamera()->getGraphicsContext(); - if (gc) - gc->resized(x, y, w, h); - } + GraphicsWindowSDL2::findContext(*mViewer)->resized(x,y,w,h); mViewer->getEventQueue()->windowResize(x,y,w,h); diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index 788a8720b..c76e3efed 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -15,7 +16,7 @@ namespace Shader { - void ShaderManager::setShaderPath(const std::string &path) + void ShaderManager::setShaderPath(const std::string& path) { mPath = path; } @@ -138,6 +139,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 = '$'; @@ -176,7 +280,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); @@ -235,7 +339,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") @@ -282,39 +386,17 @@ 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 (shaderSource.empty()) + return nullptr; + if (!parseDefines(shaderSource, defines, mGlobalDefines, templateName) || !parseFors(shaderSource, templateName)) { // Add to the cache anyway to avoid logging the same error over and over. @@ -322,12 +404,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; @@ -339,9 +441,16 @@ 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); + + auto git = mGeometryShaders.find(vertexShader); + if (git != mGeometryShaders.end()) + { + program->addShader(git->second); + } + found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first; } return found->second; @@ -352,10 +461,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; @@ -372,7 +491,7 @@ namespace Shader } } - void ShaderManager::releaseGLObjects(osg::State *state) + void ShaderManager::releaseGLObjects(osg::State* state) { std::lock_guard lock(mMutex); for (auto shader : mShaders) @@ -384,4 +503,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 13db30b01..05434a09b 100644 --- a/components/shader/shadermanager.hpp +++ b/components/shader/shadermanager.hpp @@ -36,6 +36,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. @@ -44,6 +52,8 @@ namespace Shader void releaseGLObjects(osg::State* state); private: + std::string getTemplateSource(const std::string& templateName); + std::string mPath; DefineMap mGlobalDefines; @@ -56,6 +66,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 e908b6aaa..19636050c 100644 --- a/components/shader/shadervisitor.cpp +++ b/components/shader/shadervisitor.cpp @@ -319,6 +319,7 @@ namespace Shader } defineMap["parallax"] = reqs.mNormalHeight ? "1" : "0"; + defineMap["geometryShader"] = "1"; writableStateSet->addUniform(new osg::Uniform("colorMode", reqs.mColorMode)); 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/files/settings-default.cfg b/files/settings-default.cfg index 9ff1ae5dd..d2f84a792 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -932,6 +932,17 @@ object shadows = false # Allow shadows indoors. Due to limitations with Morrowind's data, only actors can cast shadows indoors, which some might feel is distracting. enable indoor shadows = true +[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 = GeometryShader + [Physics] # Set the number of background threads used for physics. # If no background threads are used, physics calculations are processed in the main thread diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 8012c2bc1..23351760f 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -22,6 +22,7 @@ set(SHADER_FILES shadows_fragment.glsl shadowcasting_vertex.glsl shadowcasting_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 bf337cf54..71046a683 100644 --- a/files/shaders/terrain_vertex.glsl +++ b/files/shaders/terrain_vertex.glsl @@ -1,4 +1,4 @@ -#version 120 +#version 130 varying vec2 uv; varying float euclideanDepth; 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)); }