From 5b4f917cb061d49a0b304f21eeeba9e5a97f9913 Mon Sep 17 00:00:00 2001 From: Ian Chen Date: Wed, 20 Jan 2021 22:43:59 -0800 Subject: [PATCH] Generate spot light shadow maps (#2914) Updated gazebo to generate shadow maps for spot lights. This does not mean the spot lights will now start casting shadows in gazebo - we still need to update the RTShaderSystem code to actually render them. This does however mean that the shadow maps are now available and we can access them through custom material shaders. The spotlight_shadow_demo.world contains a shadow receiver ground plane with custom material shaders to show how to access the shadow texture generated by OGRE. Signed-off-by: Ian Chen --- gazebo/rendering/Heightmap.cc | 49 ++--- gazebo/rendering/Light.cc | 18 ++ gazebo/rendering/LightPrivate.hh | 5 +- gazebo/rendering/RTShaderSystem.cc | 169 ++++++++++++------ gazebo/rendering/RTShaderSystem.hh | 7 + gazebo/rendering/RTShaderSystemPrivate.hh | 7 + gazebo/rendering/RenderingLight_TEST.cc | 5 +- media/materials/programs/CMakeLists.txt | 2 + .../programs/spotlight_shadow_demo_fp.glsl | 44 +++++ .../programs/spotlight_shadow_demo_vp.glsl | 22 +++ media/materials/scripts/CMakeLists.txt | 1 + .../scripts/spotlight_shadow_demo.material | 48 +++++ worlds/spotlight_shadows_demo.world | 112 ++++++++++++ 13 files changed, 413 insertions(+), 76 deletions(-) create mode 100644 media/materials/programs/spotlight_shadow_demo_fp.glsl create mode 100644 media/materials/programs/spotlight_shadow_demo_vp.glsl create mode 100644 media/materials/scripts/spotlight_shadow_demo.material create mode 100644 worlds/spotlight_shadows_demo.world diff --git a/gazebo/rendering/Heightmap.cc b/gazebo/rendering/Heightmap.cc index 3ffc7e797b..a361bb37e7 100644 --- a/gazebo/rendering/Heightmap.cc +++ b/gazebo/rendering/Heightmap.cc @@ -3314,27 +3314,34 @@ Ogre::MaterialPtr TerrainMaterial::Profile::generate( // set up shadow split points in a way that is consistent with the // default ogre terrain material generator - Ogre::PSSMShadowCameraSetup* pssm = - RTShaderSystem::Instance()->GetPSSMShadowCameraSetup(); - unsigned int numTextures = - static_cast(pssm->getSplitCount()); - Ogre::Vector4 splitPoints; - const Ogre::PSSMShadowCameraSetup::SplitPointList& splitPointList = - pssm->getSplitPoints(); - // populate from split point 1 not 0, and include shadowFarDistance - for (unsigned int t = 0u; t < numTextures; ++t) - splitPoints[t] = splitPointList[t+1]; - params->setNamedConstant("pssmSplitPoints", splitPoints); - - // set up uv transform - double xTrans = static_cast(gridCount / gridWidth) * factor; - double yTrans = (gridWidth - 1 - (gridCount % gridWidth)) * factor; - // explicitly set all matrix elements to avoid uninitialized values - Ogre::Matrix4 uvTransform(factor, 0.0, 0.0, xTrans, - 0.0, factor, 0.0, yTrans, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0); - params->setNamedConstant("uvTransform", uvTransform); + + if (params->_findNamedConstantDefinition("pssmSplitPoints")) + { + Ogre::PSSMShadowCameraSetup* pssm = + RTShaderSystem::Instance()->GetPSSMShadowCameraSetup(); + unsigned int numTextures = + static_cast(pssm->getSplitCount()); + Ogre::Vector4 splitPoints; + const Ogre::PSSMShadowCameraSetup::SplitPointList& splitPointList = + pssm->getSplitPoints(); + // populate from split point 1 not 0, and include shadowFarDistance + for (unsigned int t = 0u; t < numTextures; ++t) + splitPoints[t] = splitPointList[t+1]; + params->setNamedConstant("pssmSplitPoints", splitPoints); + } + + if (params->_findNamedConstantDefinition("uvTransform")) + { + // set up uv transform + double xTrans = static_cast(gridCount / gridWidth) * factor; + double yTrans = (gridWidth - 1 - (gridCount % gridWidth)) * factor; + // explicitly set all matrix elements to avoid uninitialized values + Ogre::Matrix4 uvTransform(factor, 0.0, 0.0, xTrans, + 0.0, factor, 0.0, yTrans, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0); + params->setNamedConstant("uvTransform", uvTransform); + } } } gridCount++; diff --git a/gazebo/rendering/Light.cc b/gazebo/rendering/Light.cc index f04f66b04a..9752bf9a00 100644 --- a/gazebo/rendering/Light.cc +++ b/gazebo/rendering/Light.cc @@ -29,6 +29,7 @@ #include "gazebo/rendering/Visual.hh" #include "gazebo/rendering/Light.hh" #include "gazebo/rendering/LightPrivate.hh" +#include "gazebo/rendering/RTShaderSystem.hh" using namespace gazebo; using namespace rendering; @@ -584,12 +585,29 @@ void Light::SetRange(const double _range) ////////////////////////////////////////////////// void Light::SetCastShadows(const bool _cast) { + if (this->dataPtr->light->getType() == Ogre::Light::LT_DIRECTIONAL) { + // directional light uses PSSM shadow camera and should already be + // configured in RTShaderSystem + this->dataPtr->light->setCastShadows(_cast); + } + else if (this->dataPtr->light->getType() == Ogre::Light::LT_SPOTLIGHT) + { + // use different shadow camera for spot light this->dataPtr->light->setCastShadows(_cast); + if (_cast && this->dataPtr->shadowCameraSetup.isNull()) + { + this->dataPtr->shadowCameraSetup = + Ogre::ShadowCameraSetupPtr(new Ogre::FocusedShadowCameraSetup()); + this->dataPtr->light->setCustomShadowCameraSetup( + this->dataPtr->shadowCameraSetup); + RTShaderSystem::Instance()->UpdateShadows(); + } } else { + // todo(anyone) make point light casts shadows this->dataPtr->light->setCastShadows(false); } } diff --git a/gazebo/rendering/LightPrivate.hh b/gazebo/rendering/LightPrivate.hh index 15253379e5..0162a8c000 100644 --- a/gazebo/rendering/LightPrivate.hh +++ b/gazebo/rendering/LightPrivate.hh @@ -38,7 +38,7 @@ namespace gazebo class LightPrivate { /// \brief The ogre light source - public: Ogre::Light *light; + public: Ogre::Light *light = nullptr; /// \brief The visual used to visualize the light. public: VisualPtr visual; @@ -63,6 +63,9 @@ namespace gazebo /// \brief Counter used to generate unique light names. public: static unsigned int lightCounter; + + /// \brief Custom shadow camera setup for non-directional lights + public: Ogre::ShadowCameraSetupPtr shadowCameraSetup; }; } } diff --git a/gazebo/rendering/RTShaderSystem.cc b/gazebo/rendering/RTShaderSystem.cc index 0ccd59dc7a..6d119fcdb5 100644 --- a/gazebo/rendering/RTShaderSystem.cc +++ b/gazebo/rendering/RTShaderSystem.cc @@ -40,6 +40,7 @@ #include "gazebo/common/Profiler.hh" #include "gazebo/rendering/ogre_gazebo.h" #include "gazebo/rendering/CustomPSSMShadowCameraSetup.hh" +#include "gazebo/rendering/Light.hh" #include "gazebo/rendering/RenderEngine.hh" #include "gazebo/rendering/Scene.hh" #include "gazebo/rendering/Visual.hh" @@ -241,6 +242,7 @@ void RTShaderSystem::DetachViewport(Ogre::Viewport *_viewport, ScenePtr _scene) void RTShaderSystem::UpdateShaders() { // shaders will be updated in the Update call on pre-render event. + std::lock_guard lock(this->dataPtr->updateMutex); this->dataPtr->updateShaders = true; } @@ -512,52 +514,7 @@ void RTShaderSystem::ApplyShadows(ScenePtr _scene) this->dataPtr->shaderGenerator->getRenderState(_scene->Name() + Ogre::RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME); - sceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_TEXTURE_ADDITIVE_INTEGRATED); - - // 3 textures per directional light - sceneMgr->setShadowTextureCountPerLightType(Ogre::Light::LT_DIRECTIONAL, 3); - sceneMgr->setShadowTextureCountPerLightType(Ogre::Light::LT_POINT, 0); - sceneMgr->setShadowTextureCountPerLightType(Ogre::Light::LT_SPOTLIGHT, 0); - sceneMgr->setShadowTextureCount(3); - - unsigned int texSize = this->dataPtr->shadowTextureSize; -#if defined(__APPLE__) - // workaround a weird but on OSX if texture size at 2 and 3 splits are not - // halved - texSize = this->dataPtr->shadowTextureSize/2; -#endif - sceneMgr->setShadowTextureConfig(0, - this->dataPtr->shadowTextureSize, this->dataPtr->shadowTextureSize, - Ogre::PF_FLOAT32_R); - sceneMgr->setShadowTextureConfig(1, texSize, texSize, Ogre::PF_FLOAT32_R); - sceneMgr->setShadowTextureConfig(2, texSize, texSize, Ogre::PF_FLOAT32_R); - -#if defined(HAVE_OPENGL) - // Enable shadow map comparison, so shader can use - // float texture(sampler2DShadow, vec3, [float]) instead of - // vec4 texture(sampler2D, vec2, [float]). - // NVidia, AMD, and Intel all take this as a cue to provide "hardware PCF", - // a driver hack that softens shadow edges with 4-sample interpolation. - for (size_t i = 0; i < sceneMgr->getShadowTextureCount(); ++i) - { - const Ogre::TexturePtr tex = sceneMgr->getShadowTexture(i); - // This will fail if not using OpenGL as the rendering backend. - GLuint texId; - tex->getCustomAttribute("GLID", &texId); - glBindTexture(GL_TEXTURE_2D, texId); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, - GL_COMPARE_R_TO_TEXTURE); - } -#endif - - sceneMgr->setShadowTextureSelfShadow(false); - sceneMgr->setShadowCasterRenderBackFaces(true); - - // TODO: We have two different shadow caster materials, both taken from - // OGRE samples. They should be compared and tested. - // Set up caster material - this is just a standard depth/shadow map caster - // sceneMgr->setShadowTextureCasterMaterial("PSSM/shadow_caster"); - sceneMgr->setShadowTextureCasterMaterial("Gazebo/shadow_caster"); + this->UpdateShadows(_scene); // Disable fog on the caster pass. // Ogre::MaterialPtr passCaterMaterial = @@ -636,20 +593,39 @@ Ogre::PSSMShadowCameraSetup *RTShaderSystem::GetPSSMShadowCameraSetup() const void RTShaderSystem::Update() { GZ_PROFILE("rendering::RTShaderSystem::Update"); - if (!this->dataPtr->initialized || !this->dataPtr->updateShaders) + if (!this->dataPtr->initialized) { return; } - for (const auto &scene : this->dataPtr->scenes) + bool updateShaders, updateShadows = false; + { + std::lock_guard lock(this->dataPtr->updateMutex); + updateShaders = this->dataPtr->updateShaders; + updateShadows = this->dataPtr->updateShadows; + this->dataPtr->updateShaders = false; + this->dataPtr->updateShadows = false; + } + + if (updateShadows) { - VisualPtr vis = scene->WorldVisual(); - if (vis) + for (const auto &scene : this->dataPtr->scenes) { - this->UpdateShaders(vis); + this->UpdateShadows(scene); + } + } + + if (updateShaders) + { + for (const auto &scene : this->dataPtr->scenes) + { + VisualPtr vis = scene->WorldVisual(); + if (vis) + { + this->UpdateShaders(vis); + } } } - this->dataPtr->updateShaders = false; } ///////////////////////////////////////////////// @@ -717,3 +693,92 @@ double RTShaderSystem::ShadowSplitPadding() const { return this->dataPtr->shadowSplitPadding; } + +///////////////////////////////////////////////// +void RTShaderSystem::UpdateShadows() +{ + std::lock_guard lock(this->dataPtr->updateMutex); + this->dataPtr->updateShadows = true; +} + +///////////////////////////////////////////////// +void RTShaderSystem::UpdateShadows(ScenePtr _scene) +{ + if (!this->dataPtr->initialized) + return; + + Ogre::SceneManager *sceneMgr = _scene->OgreSceneManager(); + + sceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_TEXTURE_ADDITIVE_INTEGRATED); + + // directional: assume there can only be one dir light and we always create + // the shadow map for the dir light + // spot: update number of shadow textures based on number of shadow casting + // spot lights + // point: not working yet + unsigned int dirLightCount = 1u; + unsigned int spotLightCount = 0u; + for (unsigned int i = 0; i < _scene->LightCount(); ++i) + { + LightPtr light = _scene->LightByIndex(i); + + if (!light->CastShadows()) + continue; + + if (light->Type() == "spot") + spotLightCount++; + } + + // 3 textures per directional light + sceneMgr->setShadowTextureCountPerLightType(Ogre::Light::LT_DIRECTIONAL, 3); + + // spot light shadow count + sceneMgr->setShadowTextureCountPerLightType(Ogre::Light::LT_SPOTLIGHT, 1); + + // \todo(anyone) make point light shadows work + sceneMgr->setShadowTextureCountPerLightType(Ogre::Light::LT_POINT, 0); + + // \todo(anyone) include point light shadows when it is working + unsigned int dirShadowCount = 3 * dirLightCount; + unsigned int spotShadowCount = spotLightCount; + sceneMgr->setShadowTextureCount(dirShadowCount + spotShadowCount); + + unsigned int texSize = this->dataPtr->shadowTextureSize; +#if defined(__APPLE__) + // workaround a weird but on OSX if texture size at 2 and 3 splits are not + // halved + texSize = this->dataPtr->shadowTextureSize/2; +#endif + sceneMgr->setShadowTextureConfig(0, + this->dataPtr->shadowTextureSize, this->dataPtr->shadowTextureSize, + Ogre::PF_FLOAT32_R); + sceneMgr->setShadowTextureConfig(1, texSize, texSize, Ogre::PF_FLOAT32_R); + sceneMgr->setShadowTextureConfig(2, texSize, texSize, Ogre::PF_FLOAT32_R); + +#if defined(HAVE_OPENGL) + // Enable shadow map comparison, so shader can use + // float texture(sampler2DShadow, vec3, [float]) instead of + // vec4 texture(sampler2D, vec2, [float]). + // NVidia, AMD, and Intel all take this as a cue to provide "hardware PCF", + // a driver hack that softens shadow edges with 4-sample interpolation. + for (size_t i = 0u; i < dirShadowCount; ++i) + { + const Ogre::TexturePtr tex = sceneMgr->getShadowTexture(i); + // This will fail if not using OpenGL as the rendering backend. + GLuint texId; + tex->getCustomAttribute("GLID", &texId); + glBindTexture(GL_TEXTURE_2D, texId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, + GL_COMPARE_R_TO_TEXTURE); + } +#endif + + sceneMgr->setShadowTextureSelfShadow(false); + sceneMgr->setShadowCasterRenderBackFaces(true); + + // TODO: We have two different shadow caster materials, both taken from + // OGRE samples. They should be compared and tested. + // Set up caster material - this is just a standard depth/shadow map caster + // sceneMgr->setShadowTextureCasterMaterial("PSSM/shadow_caster"); + sceneMgr->setShadowTextureCasterMaterial("Gazebo/shadow_caster"); +} diff --git a/gazebo/rendering/RTShaderSystem.hh b/gazebo/rendering/RTShaderSystem.hh index ffd292ca43..61da4d25ee 100644 --- a/gazebo/rendering/RTShaderSystem.hh +++ b/gazebo/rendering/RTShaderSystem.hh @@ -91,6 +91,9 @@ namespace gazebo /// \brief Queue a call to update the shaders. public: void UpdateShaders(); + /// \brief Queue a call to update the shadows. + public: void UpdateShadows(); + /// \brief Set a viewport to use shaders. /// \param[in] _viewport The viewport to add. /// \param[in] _scene The scene that the viewport uses. @@ -177,6 +180,10 @@ namespace gazebo /// \param[in] _vis Pointer to the visual to update. private: void UpdateShaders(VisualPtr _vis); + /// \brief Update the shadows for a scene + /// \param[in] _scene Pointer to the scene to update + private: void UpdateShadows(ScenePtr _scene); + /// \brief Re-apply shadows. Call this if a shadow paramenter is changed. private: void ReapplyShadows(); diff --git a/gazebo/rendering/RTShaderSystemPrivate.hh b/gazebo/rendering/RTShaderSystemPrivate.hh index 7899d444a3..15d1b277fa 100644 --- a/gazebo/rendering/RTShaderSystemPrivate.hh +++ b/gazebo/rendering/RTShaderSystemPrivate.hh @@ -18,6 +18,7 @@ #ifndef _GAZEBO_RTSHADERSYSTEM_PRIVATE_HH_ #define _GAZEBO_RTSHADERSYSTEM_PRIVATE_HH_ +#include #include #include @@ -56,6 +57,9 @@ namespace gazebo /// \brief Flag to indicate that shaders need to be updated. public: bool updateShaders; + /// \brief Flag to indicate that shadows need to be updated. + public: bool updateShadows = false; + /// \brief Size of the Parallel Split Shadow Map (PSSM) shadow texture /// at closest layer. public: unsigned int shadowTextureSize = 2048u; @@ -79,6 +83,9 @@ namespace gazebo /// \brief Flag to indicate if normal map should be enabled public: bool enableNormalMap = true; + + /// \brief Mutex to protect shaders and shadows update + public: std::mutex updateMutex; }; } } diff --git a/gazebo/rendering/RenderingLight_TEST.cc b/gazebo/rendering/RenderingLight_TEST.cc index 7b82fa37a8..d7f2bbaf77 100644 --- a/gazebo/rendering/RenderingLight_TEST.cc +++ b/gazebo/rendering/RenderingLight_TEST.cc @@ -115,8 +115,9 @@ TEST_F(Light_TEST, CastShadows) msg.set_cast_shadows(true); spotLight->LoadFromMsg(msg); EXPECT_EQ(spotLight->LightType(), "spot"); - // issue #2083: spot light does not cast shadows - EXPECT_FALSE(spotLight->CastShadows()); + // issue #2083: spot light generates shadow maps but they are not currently + // being rendered + EXPECT_TRUE(spotLight->CastShadows()); scene->RemoveLight(spotLight); spotLight.reset(); diff --git a/media/materials/programs/CMakeLists.txt b/media/materials/programs/CMakeLists.txt index f63e65c6d8..5374739d76 100644 --- a/media/materials/programs/CMakeLists.txt +++ b/media/materials/programs/CMakeLists.txt @@ -45,6 +45,8 @@ projector.vert shadow_caster_fp.glsl shadow_caster_vp.glsl StdQuad_vp.glsl +spotlight_shadow_demo_fp.glsl +spotlight_shadow_demo_vp.glsl spot_shadow_receiver_fp.glsl spot_shadow_receiver_vp.glsl wide_lens_map_fp.glsl diff --git a/media/materials/programs/spotlight_shadow_demo_fp.glsl b/media/materials/programs/spotlight_shadow_demo_fp.glsl new file mode 100644 index 0000000000..1508127845 --- /dev/null +++ b/media/materials/programs/spotlight_shadow_demo_fp.glsl @@ -0,0 +1,44 @@ +#version 120 + +uniform sampler2D shadowMap0; +varying vec4 lightSpacePos0; + +varying vec4 worldPos; +varying vec4 worldViewPos; + +//------------------------------------------------------------------------------ +float ShadowSimple(in sampler2D shadowMap, in vec4 shadowMapPos) +{ + // perform perspective divide + vec3 shadowMapUV = shadowMapPos.xyz / shadowMapPos.w; + + if (shadowMapUV.z < 0.0 || shadowMapUV.z > 1.0) + return 0.0; + + // get closest depth value from light's perspective + float closestDepth = texture2D(shadowMap, shadowMapUV.xy).r; + + // get depth of current fragment from light's perspective + float currentDepth = shadowMapUV.z; + + // check whether current frag pos is in shadow + float shadow = currentDepth > closestDepth ? 1.0 : 0.0; + + return shadow; +} + +void main() +{ + float f = 0.0f; + + // flat red color - no lighting + vec4 outputCol = vec4(1.0, 0.0, 0.0, 1.0); + + // grey shadows + f += ShadowSimple(shadowMap0, lightSpacePos0); + f = clamp(f, 0.0f, 1.0f); + if (f > 0.0f) + outputCol = vec4(0.2, 0.2, 0.2, 1.0); + + gl_FragColor = outputCol; +} diff --git a/media/materials/programs/spotlight_shadow_demo_vp.glsl b/media/materials/programs/spotlight_shadow_demo_vp.glsl new file mode 100644 index 0000000000..71dc3e6e18 --- /dev/null +++ b/media/materials/programs/spotlight_shadow_demo_vp.glsl @@ -0,0 +1,22 @@ +#version 120 + +uniform mat4 worldMatrix; +uniform mat4 worldViewMatrix; +uniform mat4 viewProjMatrix; + +uniform mat4 texViewProjMatrix0; +varying vec4 lightSpacePos0; + +varying vec4 worldPos; +varying vec4 worldViewPos; + +void main() +{ + worldPos = worldMatrix * gl_Vertex; + gl_Position = viewProjMatrix * worldPos; + + worldViewPos = worldViewMatrix * gl_Vertex; + + lightSpacePos0 = texViewProjMatrix0 * worldPos; +} + diff --git a/media/materials/scripts/CMakeLists.txt b/media/materials/scripts/CMakeLists.txt index 9e4200dec6..f72e9f718e 100644 --- a/media/materials/scripts/CMakeLists.txt +++ b/media/materials/scripts/CMakeLists.txt @@ -21,6 +21,7 @@ picker.material ShadowCaster.material shadow_caster.program ShadowCaster.program +spotlight_shadow_demo.material ssao.compositor ssao.material SSAOPost.compositor diff --git a/media/materials/scripts/spotlight_shadow_demo.material b/media/materials/scripts/spotlight_shadow_demo.material new file mode 100644 index 0000000000..061f1f1c64 --- /dev/null +++ b/media/materials/scripts/spotlight_shadow_demo.material @@ -0,0 +1,48 @@ +vertex_program Gazebo/SpotLightShadowDemoVS glsl +{ + source spotlight_shadow_demo_vp.glsl + + default_params + { + param_named_auto worldViewMatrix worldview_matrix + param_named_auto viewProjMatrix viewproj_matrix + param_named_auto worldMatrix world_matrix + + param_named_auto texViewProjMatrix0 texture_viewproj_matrix 0 + } +} + +fragment_program Gazebo/SpotLightShadowDemoFS glsl +{ + source spotlight_shadow_demo_fp.glsl + + default_params + { + param_named shadowMap0 int 0 + } +} + +material Gazebo/SpotLightShadowDemo +{ + technique + { + pass + { + max_lights 32 + iteration 1 per_n_lights 1 spot + + vertex_program_ref Gazebo/SpotLightShadowDemoVS + { + } + + fragment_program_ref Gazebo/SpotLightShadowDemoFS + { + } + texture_unit shadowMap0 + { + content_type shadow + tex_address_mode clamp + } + } + } +} diff --git a/worlds/spotlight_shadows_demo.world b/worlds/spotlight_shadows_demo.world new file mode 100644 index 0000000000..ba3da631d5 --- /dev/null +++ b/worlds/spotlight_shadows_demo.world @@ -0,0 +1,112 @@ + + + + + + + + + + model://ground_plane + + + + model://sun + + + + + true + -3.0 0.0 3.0 0 0 0 + 0.8 0.8 0.8 1 + 0.2 0.2 0.2 1 + + 10 + 0.05 + 0.01 + 0.001 + + + 0.1 + 0.5 + 1.2 + + 1 0 -1 + + + + 0 0 0.1 0 0 0 + true + + + + + 0 0 1 + 10 10 + + + + + 0xffff + + + + 100 + 50 + + + + + + false + + + 0 0 1 + 10 10 + + + + + + + + + + + + 0 0 0.5 0 0 0 + + + + + 1 1 1 + + + + + + + 1 1 1 + + + + + + + + + + +