From e13eb625d38a13b47d1b6680608c69c20de6842a Mon Sep 17 00:00:00 2001 From: scrawl Date: Mon, 26 Oct 2015 21:36:19 +0100 Subject: [PATCH] New water WIP Changes compared to old (Ogre) water: - Uses depth-texture readback to handle the underwater fog in the water shader, instead of handling it in the object shader - Different clipping mechanism (glClipPlane instead of a skewed viewing frustum) - Fixed bug where the reflection camera would look strange when the viewer was very close to the water surface - Toned down light scattering, made the waterColor a bit darker at night - Fixed flipped water normals and strange resulting logic in the shader Still to do: see comments... --- apps/openmw/engine.cpp | 2 +- apps/openmw/mwrender/npcanimation.cpp | 6 +- apps/openmw/mwrender/renderingmanager.cpp | 39 ++- apps/openmw/mwrender/renderingmanager.hpp | 3 +- apps/openmw/mwrender/sky.cpp | 53 +++- apps/openmw/mwrender/vismask.hpp | 9 +- apps/openmw/mwrender/water.cpp | 311 +++++++++++++++++++++- apps/openmw/mwrender/water.hpp | 8 +- apps/openmw/mwworld/worldimp.cpp | 5 +- apps/openmw/mwworld/worldimp.hpp | 2 +- files/CMakeLists.txt | 1 + files/shaders/CMakeLists.txt | 11 + files/shaders/water_fragment.glsl | 189 +++++++++++++ files/shaders/water_nm.png | Bin 0 -> 24405 bytes files/shaders/water_vertex.glsl | 22 ++ 15 files changed, 642 insertions(+), 19 deletions(-) create mode 100644 files/shaders/CMakeLists.txt create mode 100644 files/shaders/water_fragment.glsl create mode 100644 files/shaders/water_nm.png create mode 100644 files/shaders/water_vertex.glsl diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 79c8c4cc9..089880fb2 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -513,7 +513,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) // Create the world mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mFileCollections, mContentFiles, mEncoder, mFallbackMap, - mActivationDistanceOverride, mCellName, mStartupScript)); + mActivationDistanceOverride, mCellName, mStartupScript, mResDir.string())); mEnvironment.getWorld()->setupPlayer(); input->setPlayer(&mEnvironment.getWorld()->getPlayer()); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index cba6c5696..17f0ce73c 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -32,6 +32,7 @@ #include "camera.hpp" #include "rotatecontroller.hpp" #include "renderbin.hpp" +#include "vismask.hpp" namespace { @@ -323,9 +324,9 @@ public: virtual void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) { - renderInfo.getState()->applyAttribute(mDepth); + //renderInfo.getState()->applyAttribute(mDepth); - glClear(GL_DEPTH_BUFFER_BIT); + //glClear(GL_DEPTH_BUFFER_BIT); bin->drawImplementation(renderInfo, previous); } @@ -441,6 +442,7 @@ void NpcAnimation::updateNpcBase() } else { + mObjectRoot->setNodeMask(Mask_FirstPerson); if(isWerewolf) addAnimSource(smodel); else diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index cae6541af..2aaba3035 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -122,7 +122,8 @@ namespace MWRender bool mWireframe; }; - RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, const MWWorld::Fallback* fallback) + RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, + const MWWorld::Fallback* fallback, const std::string& resourcePath) : mViewer(viewer) , mRootNode(rootNode) , mResourceSystem(resourceSystem) @@ -145,7 +146,7 @@ namespace MWRender mEffectManager.reset(new EffectManager(lightRoot, mResourceSystem)); - mWater.reset(new Water(lightRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), fallback)); + mWater.reset(new Water(mRootNode, lightRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), fallback, resourcePath)); mTerrain.reset(new Terrain::TerrainGrid(lightRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), new TerrainStorage(mResourceSystem->getVFS(), false), Mask_Terrain)); @@ -197,6 +198,39 @@ namespace MWRender mFieldOfView = Settings::Manager::getFloat("field of view", "General"); updateProjectionMatrix(); mStateUpdater->setFogEnd(mViewDistance); + + /* + osg::Texture2D* texture = new osg::Texture2D; + texture->setSourceFormat(GL_DEPTH_COMPONENT); + texture->setInternalFormat(GL_DEPTH_COMPONENT24_ARB); + texture->setSourceType(GL_UNSIGNED_INT); + + mViewer->getCamera()->attach(osg::Camera::DEPTH_BUFFER, texture); + + osg::ref_ptr camera (new osg::Camera); + camera->setProjectionMatrix(osg::Matrix::identity()); + camera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + camera->setViewMatrix(osg::Matrix::identity()); + camera->setClearMask(0); + camera->setRenderOrder(osg::Camera::NESTED_RENDER); + camera->setAllowEventFocus(false); + + osg::ref_ptr geode (new osg::Geode); + osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1,-1,0), osg::Vec3f(0.5,0,0), osg::Vec3f(0,0.5,0)); + geode->addDrawable(geom); + + camera->addChild(geode); + + osg::StateSet* stateset = geom->getOrCreateStateSet(); + + stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); + stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + + stateset->setRenderBinDetails(20, "RenderBin"); + stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + + mLightRoot->addChild(camera); + */ } RenderingManager::~RenderingManager() @@ -260,6 +294,7 @@ namespace MWRender { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(colour); + mSunLight->setSpecular(colour); } void RenderingManager::setSunDirection(const osg::Vec3f &direction) diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index def3ea4bb..1ddab7338 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -57,7 +57,8 @@ namespace MWRender class RenderingManager : public MWRender::RenderingInterface { public: - RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, const MWWorld::Fallback* fallback); + RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr rootNode, Resource::ResourceSystem* resourceSystem, + const MWWorld::Fallback* fallback, const std::string& resourcePath); ~RenderingManager(); MWRender::Objects& getObjects(); diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 8de8a61fc..30dc08989 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -1,6 +1,9 @@ #include "sky.hpp" #include +#include + +#include #include #include @@ -250,6 +253,8 @@ public: // That's not a problem though, children of this node can be culled just fine // Just make sure you do not place a CameraRelativeTransform deep in the scene graph setCullingActive(false); + + addCullCallback(new CullCallback); } CameraRelativeTransform(const CameraRelativeTransform& copy, const osg::CopyOp& copyop) @@ -259,7 +264,7 @@ public: META_Node(MWRender, CameraRelativeTransform) - virtual bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor*) const + virtual bool computeLocalToWorldMatrix(osg::Matrix& matrix, osg::NodeVisitor* nv) const { if (_referenceFrame==RELATIVE_RF) { @@ -277,6 +282,48 @@ public: { return osg::BoundingSphere(osg::Vec3f(0,0,0), 0); } + + class CullCallback : public osg::NodeCallback + { + public: + virtual void operator() (osg::Node* node, osg::NodeVisitor* nv) + { + osgUtil::CullVisitor* cv = static_cast(nv); + + // XXX have to remove unwanted culling plane of the water reflection camera + + // Remove all planes that aren't from the standard frustum + unsigned int numPlanes = 4; + if (cv->getCullingMode() & osg::CullSettings::NEAR_PLANE_CULLING) + ++numPlanes; + if (cv->getCullingMode() & osg::CullSettings::FAR_PLANE_CULLING) + ++numPlanes; + + int mask = 0x1; + int resultMask = cv->getProjectionCullingStack().back().getFrustum().getResultMask(); + for (unsigned int i=0; igetProjectionCullingStack().back().getFrustum().getPlaneList().size(); ++i) + { + if (i >= numPlanes) + { + // turn off this culling plane + resultMask &= (~mask); + } + + mask <<= 1; + } + + cv->getProjectionCullingStack().back().getFrustum().setResultMask(resultMask); + cv->getCurrentCullingSet().getFrustum().setResultMask(resultMask); + + cv->getProjectionCullingStack().back().pushCurrentMask(); + cv->getCurrentCullingSet().pushCurrentMask(); + + traverse(node, nv); + + cv->getProjectionCullingStack().back().popCurrentMask(); + cv->getCurrentCullingSet().popCurrentMask(); + } + }; }; class ModVertexAlphaVisitor : public osg::NodeVisitor @@ -1014,6 +1061,7 @@ SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneMana , mSunEnabled(true) { osg::ref_ptr skyroot (new CameraRelativeTransform); + skyroot->setNodeMask(Mask_Sky); parentNode->addChild(skyroot); @@ -1021,6 +1069,9 @@ SkyManager::SkyManager(osg::Group* parentNode, Resource::SceneManager* sceneMana // By default render before the world is rendered mRootNode->getOrCreateStateSet()->setRenderBinDetails(RenderBin_Sky, "RenderBin"); + + // Prevent unwanted clipping by water reflection camera's clipping plane + mRootNode->getOrCreateStateSet()->setMode(GL_CLIP_PLANE0, osg::StateAttribute::OFF); } void SkyManager::create() diff --git a/apps/openmw/mwrender/vismask.hpp b/apps/openmw/mwrender/vismask.hpp index 38fcfe648..fc63cddbb 100644 --- a/apps/openmw/mwrender/vismask.hpp +++ b/apps/openmw/mwrender/vismask.hpp @@ -17,16 +17,17 @@ namespace MWRender Mask_Sky = (1<<5), Mask_Water = (1<<6), Mask_Terrain = (1<<7), + Mask_FirstPerson = (1<<8), // top level masks - Mask_Scene = (1<<8), - Mask_GUI = (1<<9), + Mask_Scene = (1<<9), + Mask_GUI = (1<<10), // Set on a Geode - Mask_ParticleSystem = (1<<10), + Mask_ParticleSystem = (1<<11), // Set on cameras within the main scene graph - Mask_RenderToTexture = (1<<11) + Mask_RenderToTexture = (1<<12) // reserved: (1<<16) for SceneUtil::Mask_Lit }; diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 03ab58e6b..cf361a504 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -2,14 +2,24 @@ #include +#include + +#include #include #include #include #include #include #include +#include +#include +#include +#include + +#include // XXX remove #include +#include #include #include @@ -17,6 +27,8 @@ #include #include +#include + #include #include "vismask.hpp" @@ -69,7 +81,7 @@ namespace return waterGeom; } - void createWaterStateSet(Resource::ResourceSystem* resourceSystem, osg::ref_ptr node) + void createSimpleWaterStateSet(Resource::ResourceSystem* resourceSystem, osg::ref_ptr node) { osg::ref_ptr stateset (new osg::StateSet); @@ -111,8 +123,152 @@ namespace MWRender // -------------------------------------------------------------------------------------------------------------------------------- -Water::Water(osg::Group *parent, Resource::ResourceSystem *resourceSystem, osgUtil::IncrementalCompileOperation *ico, const MWWorld::Fallback* fallback) +/// @brief Allows to cull and clip meshes that are below a plane. Useful for reflection & refraction camera effects. +/// Also handles flipping of the plane when the eye point goes below it. +/// To use, simply create the scene as subgraph of this node, then do setPlane(const osg::Plane& plane); +class ClipCullNode : public osg::Group +{ + class PlaneCullCallback : public osg::NodeCallback + { + public: + /// @param cullPlane The culling plane (in world space). + PlaneCullCallback(const osg::Plane* cullPlane) + : osg::NodeCallback() + , mCullPlane(cullPlane) + { + } + + virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + osgUtil::CullVisitor* cv = static_cast(nv); + + osg::Polytope::PlaneList origPlaneList = cv->getProjectionCullingStack().back().getFrustum().getPlaneList(); + + // TODO: offset plane towards the viewer to fix bleeding at the water shore + + osg::Plane plane = *mCullPlane; + plane.transform(*cv->getCurrentRenderStage()->getInitialViewMatrix()); + + osg::Vec3d eyePoint = cv->getEyePoint(); + if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0,0,eyePoint.z()), 0)) > 0) + plane.flip(); + + cv->getProjectionCullingStack().back().getFrustum().add(plane); + + traverse(node, nv); + + // undo + cv->getProjectionCullingStack().back().getFrustum().set(origPlaneList); + } + + private: + const osg::Plane* mCullPlane; + }; + + class FlipCallback : public osg::NodeCallback + { + public: + FlipCallback(const osg::Plane* cullPlane) + : mCullPlane(cullPlane) + { + } + + virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + osgUtil::CullVisitor* cv = static_cast(nv); + osg::Vec3d eyePoint = cv->getEyePoint(); + // flip the below graph if the eye point is above the plane + if (mCullPlane->intersect(osg::BoundingSphere(osg::Vec3d(0,0,eyePoint.z()), 0)) > 0) + { + osg::RefMatrix* modelViewMatrix = new osg::RefMatrix(*cv->getModelViewMatrix()); + modelViewMatrix->preMultScale(osg::Vec3(1,1,-1)); + + cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); + traverse(node, nv); + cv->popModelViewMatrix(); + } + else + traverse(node, nv); + } + + private: + const osg::Plane* mCullPlane; + }; + +public: + ClipCullNode() + { + addCullCallback (new PlaneCullCallback(&mPlane)); + + mClipNodeTransform = new osg::PositionAttitudeTransform; + mClipNodeTransform->addCullCallback(new FlipCallback(&mPlane)); + addChild(mClipNodeTransform); + + mClipNode = new osg::ClipNode; + + mClipNodeTransform->addChild(mClipNode); + } + + void setPlane (const osg::Plane& plane) + { + if (plane == mPlane) + return; + mPlane = plane; + + mClipNode->getClipPlaneList().clear(); + mClipNode->addClipPlane(new osg::ClipPlane(0, mPlane)); + mClipNode->setStateSetModes(*getOrCreateStateSet(), osg::StateAttribute::ON); + } + +private: + osg::ref_ptr mClipNodeTransform; + osg::ref_ptr mClipNode; + + osg::Plane mPlane; +}; + +// Node callback to entirely skip the traversal. +class NoTraverseCallback : public osg::NodeCallback +{ +public: + virtual void operator()(osg::Node* node, osg::NodeVisitor* nv) + { + // no traverse() + } +}; + +void addDebugOverlay(osg::Texture2D* texture, int pos, osg::Group* parent) +{ + osg::ref_ptr debugCamera (new osg::Camera); + debugCamera->setProjectionMatrix(osg::Matrix::identity()); + debugCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF); + debugCamera->setViewMatrix(osg::Matrix::identity()); + debugCamera->setClearMask(0); + debugCamera->setRenderOrder(osg::Camera::NESTED_RENDER); + debugCamera->setAllowEventFocus(false); + + const float size = 0.5; + osg::ref_ptr debugGeode (new osg::Geode); + osg::ref_ptr geom = osg::createTexturedQuadGeometry(osg::Vec3f(-1 + size*pos, -1, 0), osg::Vec3f(size,0,0), osg::Vec3f(0,size,0)); + debugGeode->addDrawable(geom); + + debugCamera->addChild(debugGeode); + + osg::StateSet* debugStateset = geom->getOrCreateStateSet(); + + debugStateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); + debugStateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); + + debugStateset->setRenderBinDetails(20, "RenderBin"); + debugStateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); + + parent->addChild(debugCamera); +} + +Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem *resourceSystem, osgUtil::IncrementalCompileOperation *ico, + const MWWorld::Fallback* fallback, const std::string& resourcePath) : mParent(parent) + , mSceneRoot(sceneRoot) , mResourceSystem(resourceSystem) , mEnabled(true) , mToggled(true) @@ -126,17 +282,164 @@ Water::Water(osg::Group *parent, Resource::ResourceSystem *resourceSystem, osgUt geode->addDrawable(waterGeom); geode->setNodeMask(Mask_Water); + // TODO: node mask to use simple water for local map + if (ico) ico->add(geode); - createWaterStateSet(mResourceSystem, geode); + //createSimpleWaterStateSet(mResourceSystem, geode); mWaterNode = new osg::PositionAttitudeTransform; mWaterNode->addChild(geode); - mParent->addChild(mWaterNode); + mSceneRoot->addChild(mWaterNode); setHeight(mTop); + + const float waterLevel = -1; + + // refraction + unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); + osg::ref_ptr refractionCamera (new osg::Camera); + refractionCamera->setRenderOrder(osg::Camera::PRE_RENDER); + refractionCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + refractionCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + refractionCamera->setReferenceFrame(osg::Camera::RELATIVE_RF); + + refractionCamera->setCullMask(Mask_Effect|Mask_Scene|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Player|(1<<16)); + refractionCamera->setNodeMask(Mask_RenderToTexture); + refractionCamera->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) + refractionCamera->setUpdateCallback(new NoTraverseCallback); + + // No need for fog here, we are already applying fog on the water surface itself as well as underwater fog + refractionCamera->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + + osg::ref_ptr clipNode (new ClipCullNode); + clipNode->setPlane(osg::Plane(osg::Vec3d(0,0,-1), osg::Vec3d(0,0, waterLevel))); + + refractionCamera->addChild(clipNode); + clipNode->addChild(mSceneRoot); + + // TODO: add ingame setting for texture quality + + osg::ref_ptr refractionTexture = new osg::Texture2D; + refractionTexture->setTextureSize(rttSize, rttSize); + refractionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + refractionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + refractionTexture->setInternalFormat(GL_RGB); + refractionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + refractionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + + refractionCamera->attach(osg::Camera::COLOR_BUFFER, refractionTexture); + + osg::ref_ptr refractionDepthTexture = new osg::Texture2D; + refractionDepthTexture->setSourceFormat(GL_DEPTH_COMPONENT); + refractionDepthTexture->setInternalFormat(GL_DEPTH_COMPONENT24_ARB); + refractionDepthTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + refractionDepthTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + refractionDepthTexture->setSourceType(GL_UNSIGNED_INT); + refractionDepthTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + refractionDepthTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + + refractionCamera->attach(osg::Camera::DEPTH_BUFFER, refractionDepthTexture); + + mParent->addChild(refractionCamera); + + // reflection + osg::ref_ptr reflectionCamera (new osg::Camera); + reflectionCamera->setRenderOrder(osg::Camera::PRE_RENDER); + reflectionCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + reflectionCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT); + reflectionCamera->setReferenceFrame(osg::Camera::RELATIVE_RF); + + reflectionCamera->setCullMask(Mask_Effect|Mask_Scene|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Player|(1<<16)); + reflectionCamera->setNodeMask(Mask_RenderToTexture); + + reflectionCamera->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) + reflectionCamera->setUpdateCallback(new NoTraverseCallback); + + osg::ref_ptr reflectionTexture = new osg::Texture2D; + reflectionTexture->setInternalFormat(GL_RGB); + reflectionTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + reflectionTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + reflectionTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + reflectionTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); + + reflectionCamera->attach(osg::Camera::COLOR_BUFFER, reflectionTexture); + + reflectionCamera->setViewMatrix(osg::Matrix::translate(0,0,-waterLevel) * osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,waterLevel)); + + osg::ref_ptr reflectNode (new osg::MatrixTransform); + + // XXX: should really flip the FrontFace on each renderable instead of forcing clockwise. + osg::ref_ptr frontFace (new osg::FrontFace); + frontFace->setMode(osg::FrontFace::CLOCKWISE); + reflectNode->getOrCreateStateSet()->setAttributeAndModes(frontFace, osg::StateAttribute::ON); + + osg::ref_ptr clipNode2 (new ClipCullNode); + clipNode2->setPlane(osg::Plane(osg::Vec3d(0,0,1), osg::Vec3d(0,0,waterLevel))); + + reflectNode->addChild(clipNode2); + clipNode2->addChild(mSceneRoot); + + reflectionCamera->addChild(reflectNode); + + // TODO: add to waterNode so cameras don't get updated when water is hidden? + + mParent->addChild(reflectionCamera); + + // debug overlay + addDebugOverlay(refractionTexture, 0, mParent); + addDebugOverlay(refractionDepthTexture, 1, mParent); + addDebugOverlay(reflectionTexture, 2, mParent); + + // shader + // FIXME: windows utf8 path handling? + + osg::ref_ptr vertexShader (osg::Shader::readShaderFile(osg::Shader::VERTEX, resourcePath + "/shaders/water_vertex.glsl")); + + osg::ref_ptr fragmentShader (osg::Shader::readShaderFile(osg::Shader::FRAGMENT, resourcePath + "/shaders/water_fragment.glsl")); + + osg::ref_ptr program (new osg::Program); + program->addShader(vertexShader); + program->addShader(fragmentShader); + + osg::ref_ptr normalMap (new osg::Texture2D(osgDB::readImageFile(resourcePath + "/shaders/water_nm.png"))); + normalMap->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + normalMap->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + normalMap->setMaxAnisotropy(16); + normalMap->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR_MIPMAP_LINEAR); + normalMap->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + normalMap->getImage()->flipVertical(); + + osg::ref_ptr shaderStateset = new osg::StateSet; + shaderStateset->setAttributeAndModes(program, osg::StateAttribute::ON); + shaderStateset->addUniform(new osg::Uniform("reflectionMap", 0)); + shaderStateset->addUniform(new osg::Uniform("refractionMap", 1)); + shaderStateset->addUniform(new osg::Uniform("refractionDepthMap", 2)); + shaderStateset->addUniform(new osg::Uniform("normalMap", 3)); + + shaderStateset->setTextureAttributeAndModes(0, reflectionTexture, osg::StateAttribute::ON); + shaderStateset->setTextureAttributeAndModes(1, refractionTexture, osg::StateAttribute::ON); + shaderStateset->setTextureAttributeAndModes(2, refractionDepthTexture, osg::StateAttribute::ON); + shaderStateset->setTextureAttributeAndModes(3, normalMap, osg::StateAttribute::ON); + shaderStateset->setMode(GL_BLEND, osg::StateAttribute::ON); // TODO: set Off when refraction is on + shaderStateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); + + osg::ref_ptr depth (new osg::Depth); + depth->setWriteMask(false); + shaderStateset->setAttributeAndModes(depth, osg::StateAttribute::ON); + + // TODO: render after transparent bin when refraction is on + shaderStateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); + + geode->setStateSet(shaderStateset); } Water::~Water() diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 519cd5181..78e8a4927 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -9,6 +9,9 @@ namespace osg { class Group; class PositionAttitudeTransform; + class Texture2D; + class Image; + class Camera; } namespace osgUtil @@ -37,6 +40,7 @@ namespace MWRender static const int CELL_SIZE = 8192; osg::ref_ptr mParent; + osg::ref_ptr mSceneRoot; osg::ref_ptr mWaterNode; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mIncrementalCompileOperation; @@ -51,7 +55,9 @@ namespace MWRender void updateVisible(); public: - Water(osg::Group* parent, Resource::ResourceSystem* resourceSystem, osgUtil::IncrementalCompileOperation* ico, const MWWorld::Fallback* fallback); + Water(osg::Group* parent, osg::Group* sceneRoot, + Resource::ResourceSystem* resourceSystem, osgUtil::IncrementalCompileOperation* ico, const MWWorld::Fallback* fallback, + const std::string& resourcePath); ~Water(); void setEnabled(bool enabled); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index d994a35ee..be86987e8 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -153,7 +153,8 @@ namespace MWWorld const Files::Collections& fileCollections, const std::vector& contentFiles, ToUTF8::Utf8Encoder* encoder, const std::map& fallbackMap, - int activationDistanceOverride, const std::string& startCell, const std::string& startupScript) + int activationDistanceOverride, const std::string& startCell, const std::string& startupScript, + const std::string& resourcePath) : mResourceSystem(resourceSystem), mFallback(fallbackMap), mPlayer (0), mLocalScripts (mStore), mSky (true), mCells (mStore, mEsm), mGodMode(false), mScriptsEnabled(true), mContentFiles (contentFiles), @@ -163,7 +164,7 @@ namespace MWWorld { mPhysics = new MWPhysics::PhysicsSystem(resourceSystem, rootNode); mProjectileManager.reset(new ProjectileManager(rootNode, resourceSystem, mPhysics)); - mRendering = new MWRender::RenderingManager(viewer, rootNode, resourceSystem, &mFallback); + mRendering = new MWRender::RenderingManager(viewer, rootNode, resourceSystem, &mFallback, resourcePath); mEsm.resize(contentFiles.size()); Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen(); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 26153086a..de9266cb2 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -167,7 +167,7 @@ namespace MWWorld const Files::Collections& fileCollections, const std::vector& contentFiles, ToUTF8::Utf8Encoder* encoder, const std::map& fallbackMap, - int activationDistanceOverride, const std::string& startCell, const std::string& startupScript); + int activationDistanceOverride, const std::string& startCell, const std::string& startupScript, const std::string& resourcePath); virtual ~World(); diff --git a/files/CMakeLists.txt b/files/CMakeLists.txt index 00cae86d2..75cb6a9b0 100644 --- a/files/CMakeLists.txt +++ b/files/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(mygui) +add_subdirectory(shaders) diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt new file mode 100644 index 000000000..fc4706c1f --- /dev/null +++ b/files/shaders/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copy resource files into the build directory +set(SDIR ${CMAKE_CURRENT_SOURCE_DIR}) +set(DDIR ${OpenMW_BINARY_DIR}/resources/shaders) + +set(SHADER_FILES + water_vertex.glsl + water_fragment.glsl + water_nm.png +) + +copy_all_files(${CMAKE_CURRENT_SOURCE_DIR} ${DDIR} "${SHADER_FILES}") diff --git a/files/shaders/water_fragment.glsl b/files/shaders/water_fragment.glsl new file mode 100644 index 000000000..01e0816bc --- /dev/null +++ b/files/shaders/water_fragment.glsl @@ -0,0 +1,189 @@ +#version 120 + +// Inspired by Blender GLSL Water by martinsh ( http://devlog-martinsh.blogspot.de/2012/07/waterundewater-shader-wip.html ) + +#define REFRACTION 1 + +// tweakables -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +const float VISIBILITY = 1200.0; // how far you can look through water + +const float BIG_WAVES_X = 0.1; // strength of big waves +const float BIG_WAVES_Y = 0.1; + +const float MID_WAVES_X = 0.1; // strength of middle sized waves +const float MID_WAVES_Y = 0.1; + +const float SMALL_WAVES_X = 0.1; // strength of small waves +const float SMALL_WAVES_Y = 0.1; + +const float WAVE_CHOPPYNESS = 0.05; // wave choppyness +const float WAVE_SCALE = 75.0; // overall wave scale + +const float BUMP = 0.5; // overall water surface bumpiness +const float REFL_BUMP = 0.15; // reflection distortion amount +const float REFR_BUMP = 0.06; // refraction distortion amount + +const float SCATTER_AMOUNT = 0.3; // amount of sunlight scattering +const vec3 SCATTER_COLOUR = vec3(0.0,1.0,0.95); // colour of sunlight scattering + +const vec3 SUN_EXT = vec3(0.45, 0.55, 0.68); //sunlight extinction + +const float SPEC_HARDNESS = 256.0; // specular highlights hardness + +const vec2 WIND_DIR = vec2(0.5f, -0.8f); +const float WIND_SPEED = 0.2f; + +// -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - + +float fresnel_dielectric(vec3 Incoming, vec3 Normal, float eta) +{ + float c = abs(dot(Incoming, Normal)); + float g = eta * eta - 1.0 + c * c; + float result; + + if(g > 0.0) { + g = sqrt(g); + float A =(g - c)/(g + c); + float B =(c *(g + c)- 1.0)/(c *(g - c)+ 1.0); + result = 0.5 * A * A *(1.0 + B * B); + } + else + result = 1.0; /* TIR (no refracted component) */ + + return result; +} + +varying vec3 screenCoordsPassthrough; +varying vec4 position; +varying float depthPassthrough; + +uniform sampler2D reflectionMap; +#if REFRACTION +uniform sampler2D refractionMap; +uniform sampler2D refractionDepthMap; +#endif + +uniform sampler2D normalMap; + +uniform float osg_SimulationTime; + +void main(void) +{ + // FIXME + vec3 worldPos = position.xyz; // ((wMat) * ( position)).xyz; + vec2 UV = worldPos.xy / (8192.0*5.0) * 3.0; + UV.y *= -1.0; + + float shadow = 1.0; + + vec2 screenCoords = screenCoordsPassthrough.xy / screenCoordsPassthrough.z; + screenCoords.y = (1.0-screenCoords.y); + + vec2 nCoord = vec2(0.0,0.0); + + #define waterTimer osg_SimulationTime + + nCoord = UV * (WAVE_SCALE * 0.05) + WIND_DIR * waterTimer * (WIND_SPEED*0.04); + vec3 normal0 = 2.0 * texture2D(normalMap, nCoord + vec2(-waterTimer*0.015,-waterTimer*0.005)).rgb - 1.0; + nCoord = UV * (WAVE_SCALE * 0.1) + WIND_DIR * waterTimer * (WIND_SPEED*0.08)-(normal0.xy/normal0.zz)*WAVE_CHOPPYNESS; + vec3 normal1 = 2.0 * texture2D(normalMap, nCoord + vec2(+waterTimer*0.020,+waterTimer*0.015)).rgb - 1.0; + + nCoord = UV * (WAVE_SCALE * 0.25) + WIND_DIR * waterTimer * (WIND_SPEED*0.07)-(normal1.xy/normal1.zz)*WAVE_CHOPPYNESS; + vec3 normal2 = 2.0 * texture2D(normalMap, nCoord + vec2(-waterTimer*0.04,-waterTimer*0.03)).rgb - 1.0; + nCoord = UV * (WAVE_SCALE * 0.5) + WIND_DIR * waterTimer * (WIND_SPEED*0.09)-(normal2.xy/normal2.z)*WAVE_CHOPPYNESS; + vec3 normal3 = 2.0 * texture2D(normalMap, nCoord + vec2(+waterTimer*0.03,+waterTimer*0.04)).rgb - 1.0; + + nCoord = UV * (WAVE_SCALE* 1.0) + WIND_DIR * waterTimer * (WIND_SPEED*0.4)-(normal3.xy/normal3.zz)*WAVE_CHOPPYNESS; + vec3 normal4 = 2.0 * texture2D(normalMap, nCoord + vec2(-waterTimer*0.02,+waterTimer*0.1)).rgb - 1.0; + nCoord = UV * (WAVE_SCALE * 2.0) + WIND_DIR * waterTimer * (WIND_SPEED*0.7)-(normal4.xy/normal4.zz)*WAVE_CHOPPYNESS; + vec3 normal5 = 2.0 * texture2D(normalMap, nCoord + vec2(+waterTimer*0.1,-waterTimer*0.06)).rgb - 1.0; + + + + vec3 normal = (normal0 * BIG_WAVES_X + normal1 * BIG_WAVES_Y + + normal2 * MID_WAVES_X + normal3 * MID_WAVES_Y + + normal4 * SMALL_WAVES_X + normal5 * SMALL_WAVES_Y); + + normal = normalize(vec3(normal.x * BUMP, normal.y * BUMP, normal.z)); + + normal = vec3(-normal.x, -normal.y, normal.z); + + // normal for sunlight scattering + vec3 lNormal = (normal0 * BIG_WAVES_X*0.5 + normal1 * BIG_WAVES_Y*0.5 + + normal2 * MID_WAVES_X*0.2 + normal3 * MID_WAVES_Y*0.2 + + normal4 * SMALL_WAVES_X*0.1 + normal5 * SMALL_WAVES_Y*0.1).xyz; + lNormal = normalize(vec3(lNormal.x * BUMP, lNormal.y * BUMP, lNormal.z)); + lNormal = vec3(-lNormal.x, -lNormal.y, lNormal.z); + + + vec3 lVec = normalize((gl_ModelViewMatrixInverse * vec4(gl_LightSource[0].position.xyz, 0.0)).xyz); + + vec3 cameraPos = (gl_ModelViewMatrixInverse * vec4(0,0,0,1)).xyz; + vec3 vVec = normalize(position.xyz - cameraPos.xyz); + + float isUnderwater = (cameraPos.z > 0.0) ? 0.0 : 1.0; + + // sunlight scattering + vec3 pNormal = vec3(0,0,1); + vec3 lR = reflect(lVec, lNormal); + vec3 llR = reflect(lVec, pNormal); + + float sunHeight = lVec.z; + float sunFade = length(gl_LightModel.ambient.xyz); + + float s = clamp(dot(lR, vVec)*2.0-1.2, 0.0, 1.0); + float lightScatter = shadow * clamp(dot(lVec,lNormal)*0.7+0.3, 0.0, 1.0) * s * SCATTER_AMOUNT * sunFade * clamp(1.0-exp(-sunHeight), 0.0, 1.0); + vec3 scatterColour = mix(vec3(SCATTER_COLOUR)*vec3(1.0,0.4,0.0), SCATTER_COLOUR, clamp(1.0-exp(-sunHeight*SUN_EXT), 0.0, 1.0)); + + // fresnel + float ior = (cameraPos.z>0.0)?(1.333/1.0):(1.0/1.333); //air to water; water to air + float fresnel = fresnel_dielectric(vVec, normal, ior); + + fresnel = clamp(fresnel, 0.0, 1.0); + + // reflection + vec3 reflection = texture2D(reflectionMap, screenCoords+(normal.xy*REFL_BUMP)).rgb; + + // refraction +#if REFRACTION + vec3 refraction = texture2D(refractionMap, screenCoords-(normal.xy*REFR_BUMP)).rgb; + + // brighten up the refraction underwater + refraction = (cameraPos.z < 0.0) ? clamp(refraction * 1.5, 0.0, 1.0) : refraction; +#endif + + // specular + vec3 R = reflect(vVec, normal); + float specular = pow(max(dot(R, lVec), 0.0),SPEC_HARDNESS) * shadow; + +#if REFRACTION + float refractionDepth = texture2D(refractionDepthMap, screenCoords-(normal.xy*REFR_BUMP)).x; + // make linear + float zNear = 5; // FIXME + float zFar = 6666; // FIXME + float z_n = 2.0 * refractionDepth - 1.0; + refractionDepth = 2.0 * zNear * zFar / (zFar + zNear - z_n * (zFar - zNear)); + + float waterDepth = refractionDepth - depthPassthrough; + + vec3 waterColor = vec3(0.090195, 0.115685, 0.12745); + waterColor = waterColor * length(gl_LightModel.ambient.xyz); + if (cameraPos.z > 0.0) + refraction = mix(refraction, waterColor, clamp(waterDepth/VISIBILITY, 0.0, 1.0)); + + gl_FragData[0].xyz = mix( mix(refraction, scatterColour, lightScatter), reflection, fresnel) + specular * gl_LightSource[0].specular.xyz; +#else + gl_FragData[0].xyz = mix(reflection, vec3(0.090195, 0.115685, 0.12745), (1.0-fresnel)*0.5) + specular * gl_LightSource[0].specular.xyz; +#endif + + // fog + float fogValue = clamp((depthPassthrough - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0); + gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue); + +#if REFRACTION + gl_FragData[0].w = 1.0; +#else + gl_FragData[0].w = clamp(fresnel*2.0 + specular, 0.0, 1.0); +#endif +} diff --git a/files/shaders/water_nm.png b/files/shaders/water_nm.png new file mode 100644 index 0000000000000000000000000000000000000000..361431a0efc35882b56bab8ea407d245f27c879d GIT binary patch literal 24405 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 literal 0 HcmV?d00001 diff --git a/files/shaders/water_vertex.glsl b/files/shaders/water_vertex.glsl new file mode 100644 index 000000000..7d7b7b18a --- /dev/null +++ b/files/shaders/water_vertex.glsl @@ -0,0 +1,22 @@ +#version 120 + +varying vec3 screenCoordsPassthrough; +varying vec4 position; +varying float depthPassthrough; + +void main(void) +{ + gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; + + mat4 scalemat = mat4(0.5, 0.0, 0.0, 0.0, + 0.0, -0.5, 0.0, 0.0, + 0.0, 0.0, 0.5, 0.0, + 0.5, 0.5, 0.5, 1.0); + + vec4 texcoordProj = ((scalemat) * ( gl_Position)); + screenCoordsPassthrough = vec3(texcoordProj.x, texcoordProj.y, texcoordProj.w); + + position = gl_Vertex; + + depthPassthrough = gl_Position.z; +}