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 79c8c4cc98..089880fb26 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 cba6c56962..17f0ce73c0 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 cae6541af1..2aaba30356 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 def3ea4bba..1ddab73387 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 8de8a61fc1..30dc089895 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 38fcfe6487..fc63cddbb9 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 03ab58e6be..cf361a5046 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 519cd51819..78e8a4927f 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 d994a35ee7..be86987e85 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 26153086a3..de9266cb2f 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 00cae86d26..75cb6a9b0d 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 0000000000..fc4706c1f6 --- /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 0000000000..01e0816bc3 --- /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 zcmeAS@N?(olHy`uVBq!ia0y~yU}ykg4mJh`hQoG=rx_R+I14-?iy0WWg+Z8+Vb&Z8 z1_lKNPZ!6Kid!*zW2dROPg?VR<*dpSv4(e+-zRcEmD}N&*T6sh@@0lqMu&W+@+&=? zcy?xRti$3!$%lE%>=!95`dI%j{(pJCn&X9P0o$1@#%#t-lD=*QsfNsIM>~#2%nqD= z(RGdZUh|u?zefFG`B!fKe|LAd`d!EJNpp|LrTDwC8&8TnsUq1@=W4V{l9yA(Gi9i_dOCKo8t}4PA=PgqxkcCj(_tx z|Ia#mU7|##$MKj#cG1%(rxL!UFilRmlybOPO={Ejy7j){o6Tx+f808_Zo&5XjxWCV z9F+L+^AOJtHvtpL*&csqUJb~(rWLolLN_6|N<1WZcSl>FZ>jLbFBw*fZU64P*YQO1c)Q519 z)y=QkYX7}W`>v9ywP3YElR^jAqKE$sc`u%n@)fqR{-NFc{M+gGjJ%Q;_MY%_b6j_} z#x$}(u;&(w=doq(0%=1KxNTp0>IUf|^=6fZt_9gI)~}Xeyy+nF zMJ37jqH*S|c-8*|t`&N-7A?|PaMMM^H`qkh zxA*hp1eZ<0^>bG~f3@CK?99iOz0Dd6xU@WL{x)#g{z$vrx-TeiQB?kk_U4Nv<`)lk zJYxJ)EV8;OYwsP|U!UFv@4BEaHPM7|ZprrgjySu{JO}mjTJtP#uFVjzei4~sT@`AO zHBq{Vb#d2%mrY75ZftSlbh#+`#eIXEBhL?wzdK!p1w%BoHa4rBkP`_|+053$_a(t8 zf0z16wOuFt;>C7klxZkExGuJI|Eacl2iJv#%$U7s=5ECWizGx`9mOSG%PubN$g=9b z=NlS)(cW(0? z*Y66uJ(?ms*QhnTQ&_Dq``w~VE88S|Z5G%y{Sf?hhexj1Si2(hLY03A`^_${-A&%{ zm*jmfE;D}TEmU{t;x38H=7qlnu3VDt(o<@C_cq)?O?GyiqkAA@a!&jq-bj_D z8*a<1)0&hg-@vi=PLD#v2UV%J>AOlTk`Bx4Ur_e4ar?X)l@wFQOFCT(Zrk%b-NCiu z&Ko|r!*W$RcPBXWtGgL<%W;0yvY+=s{X+k3*;C=7Jo~?VIGo>gYQYrS7lKxj8&7(z zGT$hq>d+A`Ao%24eS2I&xS;d;_7=-VdB;D?OfK-udbW|>UMQ|FD%LS%(%mC|Mj}tY zY?$<7fpFFILt4#B9%8c1A6jRb%)c699{>Ee;D&B4+sXI5w$Ht=kRy`S@XM#jbFwQx z+&8hQ`D~wH|0hGL!fDNK!^7Vu=KlZgcYNn;$!cNSDe;Ho_x)Mm&92|3dnhi2-!AF1yJ|ekek%#lFIg)> zv|hG&eONnN!!(uOD%w0^rg(&x?}}wR7yVq3RQ}rF^(w*W`|6hi*<&9rI=NBw*Nq0H zh;m1hYBTG^zZb8(KX9SEm-VmYk^5F0$*e7llV0;^a=n;*jzQ_mmCdvI9JRPFt<}D{ zTr8t?dU|e}+0>(bX*=Rx2)=O-vR_bVVmiA^W%mw`Z&|-2uY_NSceD#Ff5Me!e*Kfb zw1a8n(Q^xAb}KdTs2yB4`Lw{^=i{>1uDv%f{)+1uxIqt%?u{fBmymXVImN zdQ+98wYmeXeoSyv-}u?v z&rp|4x4P8A?@;&qQQ^M!e`)EL#eaQezmm^5-|^hF{pPiYGRj^sx*R%w%{ylLzrtPL z|4K|)ExPZ++w=R16VrmFpM~8>Hha$~Xeay|b3!ymyb${pNEuNwVIBYGNEd}zKJ&)?-Y zgIlXrvd8^(y|t8F>G%Iu9d}GmTs^xkt>$2yLDC zU);$t|HZ^FS&VjpAv{eJ&KGyu z|E<)Fi+=me%Hs5mGym*EPR_e$z_QdZ&W_=9ga6J~@~>vcJG;iU9++~`k!R~9Nv)bY zPqinNU$&KaTX4^}t&a2CWnKBp<;Q={USZ9Y`g+5+)h|S!h;Nu;7v~qdx_cMfMAns4 zg6H;xh0e9UaX_PW@3}A`*`*~74O3=*czJkJ+;iDiN1OQ_Og1K#8+~>;p=2~8Foi|d zZ|c9Pi$lxLTvMDWy-p=9IDDh>S390bt`a)dz04=So$e1&7h1Z1o@`9@heoa|JqtE> z-WQuX`JcwCmweeKwv%p#2Q=?a;98vJV_&eid<6C;K2aONQ}&`KS0o&{!v8MJ zp>(42l!^Nm?YbJpm>G~+UbuACSBusXaVG8W(lv8jV_r!-l6SRnHvfF}%~Y@W94(eB zF6PXQCVN?`c*Q?k&$gW7-plYV(B;*&dcGH2`?m2NC@Xj3EbU$^Vy^k^lJqH_s(h;^ zsiUIHt8`l*YGtw5hH76ieQ>E7hZ~@=y=9wc;n1Y479?{I8x@>8Ti5$TjK32e3B)L%+C*QoNHNZ+XcJs2u8E#9Pn=n zKg6}SduQyjl$YKva};|HHU+GY=bd`;UhVx~_k7)6b+36SrZFY_bZ_+*{>J#c-|vfs ztvk(bUG@B8HNDkt!3D?5!3)EEuN5p^dtdqCth&aF8=7u$Xx+_PuCbiq%D#PWMta+m zr(eGQGGh+g<*yDq4*dFhe?#3ty}SPwh~7L^dH;X>y4gl8^ZLwu)*U$N-y1t6EJ*gu ziUk5UmdtGB->LZB=vcJikJ4X<_~h3m@)+^D@;8QUZDsy(!0u`dXZwkSAm_L9_wjvv z8{TI4zrnFEw}Pcc*2cW)mgBVxspk*gX$@g1{) zQ~SE=;M;SLOICmR;3;Vrk#)RE%VAAetw8k4h2E|aKij5uZw+6@X35Ox?8fcB`up=G zhwnGdWBkwjHJ^RKB}Hq6h?XGci}M`!yk>|o4mq&?WmC;6U*&|&SJAq4^_la?%6#jF=E_^0S^xGGCg86UG<;V!O>)@2DoJMZ$o2(y{y zSM=T@VV`H>GSdEL<8J7sTx$cwuB7Zw_P?m2lPjsL3A=H`h^J}vLF&sg;=KhE<=o-usG zKlWdl0Y_u`<%)XS9!7e)@019O{k)_?EQC#GYx-ZFtu=!0LZg~j6tO>hmht@OH1??% z8-1^{+ltg$Pq8X{vs*iI;-9yA>C(nGY-j9Ut{!!;cFN|H^H`!M{|d<2acBAeZ#i*% zSMBt9}L*7I&+Ji$Kvt=&ebpPEKAOL;rOdNhIPL5zpgatqf&te%cCb* zJioj7$oK!Lh1(nMdP-?@M{O{z;JPzE{l(%vJrCX*cZEH?uBWxQhQVvX%NvgGleQJz zWAYW4xkn>z;+uz1=Ck}Q-; z@{3lix~JED?V#Da#tCeWfiJ~&-nlOxduX>uMe2t$%vw8Lk59fm{qkEbt+T=(T&>Q` zif7d}ePA`A)L_O&z8vR^-3$oO|$@=)4@2T>itfKo|3eRhPE4y~LYm({I!}FNqcug-A z^T;k+IoqO!!%uRDW@^-;TR+b}K61CFtk`sqPsfYYG~eH|z9xS!6vKc0+Z-?TZlga>$_DeEh0f6lNyok|(e{gU>06feL4$?Q^+Q+E(}Y-6A$Hd4 z4N=8iA9=RV*%jEgIqee9*$;CoGq+an_FCARHtmG;96c?*2bbP3#tXAJt*BkI^kjyM zVYKsR2i?z-4HK)ryslO$o$fvLc2?`}J6aakxs0~|+bI9HDE;O>n?j|AzOc#5{rB%r zYH~K(8hdv4*M&a?ms!eAS?BkARegNUth&us?>48ZAUCP`wdj8tln9-q&#jKH-EBp2W8Pdbw_b=8CG8WPQ-SyQvRS+7RVZM0(f$ZT>a zuyJ;5Ta5R`1oo}ntJ3z*im7^exqY@yZ@cq?^PibtncKI`{SZ2ZmqYylK%SX`|C6N=YJ6Y z;#2f+8plOTtCLyc30f~C*1g(YS$lf{^|bu(mSq&KD^<6Su@{!I--A1!ohUrSGSZ+d6VMWqgLKL z%YIZph9gYamiKR$i{hr1OP)E(QLXKoOP!Y1EMD|w;q=d02eUalSL(jl@i)%oh~yKa z%U9PtO1o(K<&ez`#ikQNoUAgtmS^U#Jan>V+qc8nUpFqwch&Y$Hgek$wo?4b<{vB4 z-d|GK>T)|MeDC|4f3Mdcye|6Y^6{-RQBfnm9kX}gx}1T z{UD%O=5r$c=no^0TZ(xu9S@?_W^G#@v(=>KvhJKSaesOLWNr@1JJsbd;{ta;;**2E zIdUw!y{9}EDYB@l+E;hOSS|F*?!7PbE}r~-oAH3j@*7vej_$twGqrk7WNt=9(Z9f{ z#R=vUHB*)bZOIQ_QTEKd)A0{VC5iExe~%)%MGI!-Z2b($@W&_Sw76KCLbj8Zga@Ce6CKM7j5eE^tX(o?$=9)RZbdQ4t+5@@7KNP zgIP2D=45?utNhb{^Utkkb}CP_p2;0qEwT2M#kaccN7hZ-d`jnGkn9#$6@mE)_viZV zeBF?g{`qs${yK>-mgjApWA^08Nj^Dw`sS2Mp=yaYC!0Sn4~}utVwxf28mCoJe5k_V z@60FOrqQJi`C==#Y@74^&ev3tV4kREv5#+`@o%m9p<+C1j|tN|`3w9K&Y>UvO#O17 zcmJ2CElC``t8$lQ-S;W}x_e>#V;htI{O2SVwrg7dJ0RGl5;|M*(OvU-PE}DBF-yYC zCoc*LE#(cAlbiY`aY~Zr=H76_c*iSZnn!JHKmPYRzp(k#TMdnU`#ya!t1Yy)x@P<0 zr{(Pb|Cq0}?|1I>{Sr9gqS*e7Ye65EJ)D0ipZD&!E|u(8zuhO9o{;9(IG?NVT&>Q; znCoBO52K~)zV6R1k9Z`pOX%g%Cp$}2{kYA2?w=EyAf&tO6zgy1NBp9S(ibe6+G zEvk7$7nz2LO#OZ6W)RPN@*9>U_kQQ?}e{{m6~z-t zdA*B%R`75Bb@R;fm5;1#syCLYE=ixUbz8zIiJk~02Os%c7c6$HOZ(69=&~)p1663G-~~3C0q$e7tQQ*`1N7+1ljLr z;v#%v+@A<O%tA<^Z z$T>qi++w=4o%g~@!#Rn;JPUiiE_2@f_Y7+|`^TJbw`LvQ9eZ|yhwJ8+#2zQXgm23h zb;Le<)jeaE&732VslUGOcu*zQFX8k3+vARKo~o&-(N*)T7(P4O?EPaB&L4d4tlF<^ zHvR1jnQk15W$60yX5Gm*cUGKD`;gQz$X03Tst0M4HP57H583_`-dH{IA~=eOrWH%m|y6Q@?EX={3#= zmNHjF*z_z5N{;qjov{4bRqJ+Z?yqYlg_fRKD){yvSJ;*h-}W}F%uv{-`SjZ)gIH~Y zDXVJ!Xg^TjU?VDhZ7I+4mhmD3c=y%%$fDfI@xKa0>}S31Ppfh8`}nnQ-Ft^^ z`yWXsUJkzaPEl-Pc4OY;Fa8nlmSwG2=efq!L$U4owC8&kt7yN|ymV1+#p+9c)K<-S zRuE^>uM@Y`$E1?G_?gzm!1#-2Ha_6`wE5HijQ^H_Y2FG z`lU=}lWY4`Jb(IJ-MGKJv-5UM(O%y^t6gVnYIXGUZ&7*ekL)L&UMs%OTlnB!)x*DD zbDrn;HOVgE)bj_9A1o)`U)C`>QRUaxFPB=Mb-&r!|K`>F1Mfdnn8-g~`!jfEP)NnC zX>w<03D!jHKKMX!YVV8J=Eh!{h3!#UW&1qMkLg<-w-uPob6U~v@{!>4Lbo1Q8)r8? zE7susv$s3PH@whmj6$(fP5p z`Dx?H6XvIX2!60Wo6Pp}@cA$6(|9W0rCI*5Pv2bU@b_@*yAKxIe)~1M@4n1-em-BV z;A357;g-XPC7h3$O^wyJ^p)6gaD~vtZw00zA3x;Z+ zGktKgwA(-Kz?;ZZ6XzeC<(g&?Tk*WTrJ`U=A?FP_lu4cXg-tn^ubiEG_GDN z>q`;a{dVjqEiG|3{~$AMiLZE=wQYdr(rIRq%CBsbPE=jK#XsY#)CYT+sPaAjdp`bM zEy~%Ze6@tpd|JmY=^tH5@dx+$Jb6;va7w>+$7ZfCbNBU~I6T#A`+^vw=vxzaAD_8< zC-(~Jg4EdyQ{!7tM(fr8EBfdD$GY^Uw9)ewExsu;!&fK2NKs_qPI%`&QT$7UP|%Tp z^>>|3bc(NMSMSa?_|BUWocAA=YP4X=+nZ-UOh4LR_xUcVM$g<&~81RZ``^) z`g-k?lY5RD{kr*d%cJ@`XV=e5vVAVlam9U}s)5#g^KvQmsB%dzx_fuEZ}1>wWtVns7VJiEib-w%`dzJNHw0fRcaWeR< zp_3Zl$4{aaX2~z#f0lNNaDJG#yz9*3$_hpQrboWXr#W638GgC=hTr+X%H=n9ORP_^ zzdH8|SDN_d##u~pe-6Z3|GfIXX6?mK1*!%T?coJV4eTka*qgW)PR#wWTVs*hW%k8> zUmOxbWRg4GOblGI?QI0wHuk4wJ!`^AlYRA5wEWTn6IE9-`Q$k-O?$B9 z`R~wot%BK4(+}Q~VlU?_k}-NABIFv>sA**{Y*Bm?(d!6y}lwEQ8GxwB;9QH}xxm4z5&Ej{9 zy_X3Y9@(mqVrA&$a?SMY+#e4EPhMO4Qh$b_sFzAevF9#7ZsBW8E0`aM82tO_^ioI4 zS5(x#)%L<+r{mK@v|FcWGbOZT+TWqRieHK&W)K0Q5=dcQ#HzLlb#?#%zoclKy^ zEI+u^tC**)-NRW+dGjD7B^iuHy+a>b{A?T%pB%=E8E ztSLNGdRpSTr#swl7S_FA5>dDC=`MzKscxSGKg|2M^JUppZ8G!w&{h(d`K{lFjEx8(-}!z3UaBF@x*ArOHd2x2gX*ja-(ZG2Ma#2I1jyn3pW-cl94ug~g(X$&IT^*1zDcIYBr6&F|2&p;w(hgfu5%k+3h0UQ zX6-Ni6Qa@6^N{)CcK5(iFW<`o$sOS{_H^@K(wU>;_OX9P%~F^75pH(YJ5u%>mOHq* zP+}URNS^=hFZX+|bj!b6zIn2{!~)y<_NLVptrG6XUwahnIV#=9k($c1(AhSkOlswB z$zM4YQ$6P{?aaKMpN?NV73{yzV`mF;`UKCZ-Uk?GIN&wsim7jemE z@`_1;ho|PO&gOer@npyDOC`T=E;Wds`^Rin{kJFYi-qIb!fn5-%8UE9were@q)N4i ze2aq~1W7sdhJW0cI7?35C$W2?ano}DS|uf){M8eEW9^k=#J}%wG>k}|bFXLBhvL=W z7-KX=LIf7-U!Hhre`?M{`{HW9VE6FUh92gqsf)f}J-erEMF>ygv&qlCe$u(~(>#Vh zgiSl)gu#R#8?0MTzKMLb_;~J`3)^?IMO(h;vExgdF)i+X@4B6DGw(U&-o5hNInjrC z!C}2=yTX@w)JEUrIeq@$O#`EfwjfTot4ovvZrsn_Pd!)l_|oJX&`9q*$Rs`)xOFn^vn^3fu=DKJ8lFV86d`WjafB zw7}-5Jx67?^DT;hrIysW`$Bc}m8f@XyIx+GXWwPE(e-iM4MDNj$JTDW>%8ZnA%FPc zZT6yHyp;MLPM>jHXo8sWYo+X)*I48h+wAiFy50GJ=dIAq6RcgY1nyt;R=P}N?ovfh z-m?$mRW+}jv|hej;ZSew-K_^IC4RJB$*E(CPn~c|tiYCiYc8v?)1hvOFB)gh1$G=` zIcKv;+wgA0?AgpW)QYZXrJP@QOXacb%|*KZxx8<_wG%sVyWnO>57Vc(3{7Z zzd6sYS?Y6A(8&au<>pGW`Z?M|`K2FANUWT2tF(o+aJG4=@-*&cef$ecF1LADADY$h z!%Y3&oexg(`EwVq$eec~+<)y%Ifwi9!H*`~+te+3Bw^0Ml?r_B`)6g|zo&Oz_|NIN zTjJh@)yo94@wST#mVL0gxO(}qPwN~0zf72ICMN15?5*kfJl#8S>A6)c5A{WTLYJPf znEh8L%R_u(ds_Y!!<@&$ss}RorH}V6ID7Slcj-BggTZ$_uP2x+JL~zoFyQWo#2Jg* zZt7iQ_@WYYYnL&*{iG+!OoDm3zvh+fZS1-E`?^p_rGr2)?u{l-`w{# zPdBwjeR->#dGh;&n@NXsT-WW4T_U-AL35<<`e&OTJ~uKfoq2A%sO>i)?x=b0nx#RV zqIdF^TQ%=vsroCLzU_jOopbAbpS^Dk6QYc4;;h~ky$x8gUMKLhvu5Jg=<^cyQxkMP zP3rEjGP=s>v%PBKyv@tLxXK1x{*r!AVMcxLQ~McdWxj5Ez6j<27T$gBYV=JMI0x+CO>8XEwCGTb<9dK9{k4*3b3a z|IVc>^;sCPDqUWzzE31f;QDM+(P)KJHmA;Sdz;PqAoG5m*N1fnF6{W*C{f|HQatL3 z??HXG#i>W@m*1=^=IDFDvuwhNNuQ4%+5F4kx&7Y{Vkf&N&1~T8>MEXmt1vCPsnwgW zFLvd#1);J$_j?q6bfmco28(>Gz2d?DkpH8zu;;P~$8Sh|i4okh=6<(c(Q?^iZzA{f zzf9b}auO$hv(CxeQEg_-nchctYq`i-ZuM^Ex+XVE=bB;bG})T!o{eUIESFe3V~E$> zvocz)R`TQw*7YfG*2?^As`#24anQV~rhQF$zwEh(0)5UarL`)q2QYf5@BI2m^}f8? zS-JLU`GLRp@m+p&dtdk4pLtJ8bzcQ6JKTD4t<;I=d9vGQ9w}5eTdjJE`Fic+dyT8U z%sIG&<&F;j#3yGip7GgTHuGADkX20!*VejO*H z)rOV3RCcE>PU2r^=b<`jW_aY6!e?IpW~ZCj2Oib^T%fxv;K|i5hMQmJ`laswE12YL z!(FF%A@yf#_@SDcHbMS1{NaCB*<#E;a{KxYIb$)pK*jvId@3}%pOp;Jb z_EV+j7gYB(*{X=Vyp(%hmC66Of|sC`mz!)Z)BBAVjy{WeeC<|dnBn=1Ac;4(=RK7? z?jPZqUw$ISknL;ix2KAp)BQH7O=RmXx}olF@U7Tvef7#lr_z!;c}4EiD~kDIIRC%f z^Y05&tBG{v(U(dR*Yu~QhTWO}{7+r=gPm8Gcg=C!H|gJ}KZn((d){HVZh3vK=k=ts z58}f={Au}NyJF>wWmay?t6xXlxpO(^eDEuiZ)$I1*|z!=o#f3v9kxH_?{$l773Y2~ zoUYgY$y?6*vC)ZV9nxykj_lC=$?{Tuao=OZ&3;@z+}^JGw2brKhL)GNcArlLIZlVqMeUE!t07K6D@lc$HQE4;jO zolMs4oSDTdA6;>q-lCT}{lXnD_1{-Nm>~;^=5)+A-}>Fc zoSzDP|J-<<`0BiIZrsMZ{EqFK+~;e|Keku+7e*(3WtjSn?Lpth%P#enD7f@!qpaXL|X?4IV8h)zbNwTCUgk zI>Peno4Vpv-ph-(+}f9Nm*e({XE#6Hym@<{YxRkOPjNR6tj+Uk|F!Q-`3%kad#C^X zyJ_I!b#kGkL;Zmti+9d5_?Po(^-2BeU%WTO|CDxGZn^9{+jyo;*4@t+VkP7spINu| z{@h1@6z9}!b$#=RYv%gIT&EY`%9be>9Gve*F$vY|4+A+N8WdXWw*JPi+^r*q|&2J zl}9#SlU#PLUEz-5{W@-+Ma!*?uCfYBP3Totd3i-Z@9R(ZecLy0eYrnS*E439xNrE0 zi$+coTYAK-T`pyGn)pRuUKqAK_Uh^K1n;J|te#U3uAg{h^Hrn0cLKt1?exf7#3Skb z-N8PanX^2Xbf5b0*`r)c$ZwW+{mi%0m5-&|xv!R;6F8wdebQqNv#5}@?l;&I zoNAnGW}7})7nvUPJYy_pbf!|_kH-F#xy zhYig=p7ry8l~;W!ZVTl5y7F!}kNf*6<_|xdl3!YVK1bKcW3PQ@3S&C^#_Z7t26eRu2ikH2g$AHFclir=^CiryUAIPb+`S#6u*W_!#svo-m2 zvg~1TUBn!AtG?GOJS9J#UHKs`s6x|^U%N!b@YFSJi9F8ZmNv^hPYJ0%5IZ6DcF$U~ z!`t83|7!n!-uT?7dxDS7&wJkL^~hG1G4IdOv^%kv*ws9*TsWKW67*1_r26gs$VHZ> zr8fHiKTK+S_-v-g?o-c>95qy(|BvH#YeaZK>D{)rNuN)qXqJDJd30xPWIb=|FS)9n zSE>zDW?fL2Usui9^LfV`sjM@O!P>3cJ9fFwc>8K+l(chpw$QRw?`N0oe4O|E)So>? zcax<{tjv>drm@aUa+7}@R(ol6>Vt|op6^p>hUUkzigix<|58wA{=>q}YaY4T@~5tmvDDeP{YsroW@T<#;JM^(H|9CK4($9T=lE-fS)Ayb{5IkJRuyKcN_j`d-f%8*5OQ*T-<2m-~cADyh zg_Ct|_}9Og@+0W9;69PFYcqasbJx#H?%d69HT_-Ysp?%_U#4t%w8LC}P5e8B&~G`X zjl-s!zJGhVJb#YH%3B|;f;M%mJnGi`Xx7qR#lk&bw#{8pQ(W*+ zqB45NJ8cPLUDkT-#(ZAI@AA*zrwP6gNZXJ+S-JJy(Y-fc-eW6ExV&v+k?h0N2!jph zYvtX(M9KYX&kwkqwb(?>?ka2VlolCj&AES9{bs3NzoSqwC!gaZ`(9;*D770#5u2|E zsJ&UcVtRCTaS!`X{vFpJPvM>0wB|l-d={_$;tn4>X5Xd#^uSgB2kTW2-imkGthvt5EDA#dBHVAMf7ld%Rux=*ge=+Z1Q?@5tZI z@z!2N@tOcn-pPpjSBrMl=3YE?|6kzu@7&kD9fhN7I?BGDUww}yBKj`>>$}>wwTt-p zgv4&lICARu)77i|&wqYmYVfUE?DO4k%C;XSPCT}7%kB+pQLe1p57 zvFiz$O~(73SLQRASz4`5@0%X=U`~D8rFN<6SKR!o`<`zM`aE$J>+Wq6jZ>@yH?%D) z|72LDKfk7R&1d~1-ZE|8kJl`;&i>+5ar}k3wRsM^bm@mFrQRQ3g?>MEuhui<_t&={ z4p#nj&Umgl?X>Hoa)Co97sX_!Oz(Dy{`B?J!5h;am~O89{9R*S|1x>q@>3IZUw=@0 zedB<=;=e|g329fhU0rUPUpnd1z58yL|8afFTs`&e7t5#4BK#|hZl?u2P34WtFF(Ec zoQrm`w6Vd%k4xHgjnZ5*g42%07O+-sUt5uJ^>3L$;?(OKPt8c0F6JAj`_V(d=HtGm zm3GyA3U|(J3(a-By)q=^`<{b(zs24pZ_RFg6H&zSeafdFKlTgF{_@&m>EC<&Uxbda z_VK*=!F>GP_h;KThaOP7^~oaa;`O~V*G5O~oGClowtM};(j_%}{HJhncZzMy%C1~} z>UCjrr?7a9=6Ynll)@Z;U4|&=-w%tZttr<{<`k?T)SPL z;{GnWxi>e&G)mzJkUYrCSb5TW-1^W{}=dGGXVx zW!t&GryrhezRu@a&Zl68dK;VT<+)dO?tS%y!Q|)J7Y`DI?;mcfx2(E#!ux*(=d1Ti zw{1KWz4_j|LuT`CY>!ad%laGvu<@wn{&uGa{4oYqJ#X$4(Go0SzEJOVec`$ z`S~}k{P+Gj{Coa(<*b{Yv59%9pQnDvW_fLW^tDd&ibQ3e-m>H8X1~&NT738J+*7f> z5kR%w%K#>K3lWT*ERO}9I;t^+U$tr@7r$g|2%u*wxKvf>evp>4kJ@8I|1pk zw5+Xr#DnA3r=8h8rT^Zxz?ZkLnQXksY?g0x^jcx+`o;IHmqe~#@!2Z;=ZmR5Q;Y7O zlYBdUeyjPy{d@l@ZQIZndC6#r?kD3L!Czk2P7D8h?epzGC(AmPpPWAzvc}YiE;jL( zIIlMU*8z2|3#mmupQ{dh&YHKaWo=*e&0{}pFI?$Qo}FIs?#8j_8}yUER+krwN}4_H zJG;(zS-c(ht{L`=E9A;twU2n6cz9N3Pwb}5#1Gm}cRTYNhqiOCKKMoM#7fb}uMSUq z%6+-&-=2w)94ljz_fI!zy}3E-^5c6~Dot9i-uG(O?e8<{SE`Qd&{kW+*oC#wv@=B?49*J0E(7sgM#606A!@1tO&ML~>+j+9=zt5K0{pF9}YES9(4Bl_n+go&1Bcsw! zO1#nT)1P-`dFPviTVbgcS{XI19+|IqUsCDZ(s3xdJuAYvK4evX+v|*rdyZ9J5c4kZ zP+96K@_p9U;;Z?O_a(MYRI7WFpX=%~zvOT8lNpb#6!R-zTwQ1FGVkc5-xf7;3ooto z?s2>_vF!SG=Lf98Ih{dy|L5lv`Cm5aHhgL+vfo=*^*oDp`Ic>cYbM^?IA^< z{@PasPc})kY<4}W|M88C$Lc9(OtdB&|H|Ovzqo96WaV;uSC{B_6H;ze4H^ZT`Po!0DF?C|#Q+3cHNMcjXFNieDX(01Wqz3a+zlAqXCym)M; zR`UMMp_9v}X+Pgs<@@xV>4ViNx=rPuQoERZ=Qo}!TxqrG`IkEmJ6~`AFe%Q{99M4S7UqDC;y9z5^U?=G{5*W&&^c-wk$87;MLY|O&6az zt$CoO->9{5{iS#7Vsn*OZt|S+dGB4WE!bT({INgW?^vMUJL%1#dTB zIXV4wH{brKJ^TLVE_7L1q;~g-K;)v}DT^(l)b3qc_vYF5@Xybis;aDi8oIi$@NYBd z+<4d|JNNdFpP5UpKfeEu-MQt53^Uc zPAy&cxAMH&mE;F0&CAz(_ptix>;K=5@7(9}POVcu2Du&N|9{;+BB{` zs}sMB&rN1k%7Lek9-buiTfdduO_*2<}Vpd%y3p+vjJCA8579 z+*3R4`XTh7)IFiFs8+e?)uBfBFJx@bVqL%dV@$kaxy@|(yMMlNKihlt^M(x9mK~Y%SuOn4Nup^d&;2!pkELJNN%ad$EBpMp@=nnw=jEQ|g*K;(r#`MKTO|4ZU54C+ z!@F0Q&Y610a?u)*JT3FToPYSf#xu(QZkcHvevU6rdL85Ej~}|Dne^o{UOLRODHPZj z-dc9A?9xK5Xp`kh%d~>8<~uVT=PJvYsTqE^=DfigF6DjjV_$q!?AGzOHCnsRR74Bi{5$jCg|6=vs$VB5M^?AT*9WhW)7JTA z&H1GE0H6GaFHdDl&;Ncs`*#&<&&Btm2F}fEdVAFCwrp?uWqaiH-l;8q;urRLwJu(5 zmtlKvH`kAhZ}N`+6|Z03BQo)jY3rw%JdSc@f%E4pKU6$>XT$8)6;Y+$Q^F2Uzq$8U z%44=GpFOtt%Wb|Fzb5SO1)DcPyR&+=>~{ZLz2l^!?5DO$zO7RYn?EwY5{gfl|7s`i zmOEUJ_sE_uJCwYCW&3hXg_s-1J}Q3rkiyaYV*Tzgp{p0)X5O8Bh41n5KT3Nh{=O%3 z>Au;rW4HfW+idm8cmFQ3x&7c`5!HX@hLtgnL zb7XzSzD@G;5_>jZ$h{G@PvYsT#V&F`m(1+0|DL(Q@8jCs=iOVX=e0Q)zMW<@>tC$-Ubzpm zid^sK>2A!8QMk+(8GTFtug?1v<8Z`*|D`8TRPE*=-2Aluh6OMeRIwDP|N z6V8d8{k!JW$_c4YByRsYapeHp55osnH_g^wydeF~{wTfdO3mIU>R0-g$w@`+ee^?P zF6WQi)rs>`!!w^;oAP1Bqk~iU58ZG#7JH&8u_pU=T2yD-(y6K+zdXI=yJyeVYFVqP z8c%AkoqG4l`HFx`^eMAq4YBa$2j*&jJ!jjF-FxPpulK!;3hA)((z{>8!?|&a^Xr$d&Yu1Hu>9X!;}u4(dvX%<|NnZ- z9dY#tYqm*0=huu^3pJK`D{elqx=CL6+x1R4rkXgvWZR}Xo=3u;oC6o^eg4q(lW6JX z6aCjV`3aX~Tlc2TVw4pr-m^#Qn?gXk>B+_2x{K3=!)ckqtyKR!KMwVZ`r-TpN9 zhW8l(TW37}vZ{+|>Z1Pj-mN~|%-0+;?v$9X3NAev$h)`e-=r4pR~!0lznj@B$~m6( ztzr4iHQQr$alx%MYh8PHhHCszt?jP5%6aT{F4y`GnfIHVx%Pf-+p2QeXm`|^Ela8< z@?X9pzoDLAZu;V<7bevu9TPxx4Ab70c#dm$A69IS+rHFiU#P=z zH@#2)viHmjGwojSZ1eTRm&a;3&Y8TM`eFKBCaWER8!`?qwcN{(o<_9@4u<7!uR%ilKVc$t^w#~mEKyXv9b zD@mUwJ->IxwMDHwRrS^W%(}>vw^LV`UwU;jfoEZG^0JQ^`V%)z+L8QQaaO4@w_fY3 z{dS45X}{&zrgQIQXD07p<=|xj?^X`=$-Mf~+ZGRndy9`Q=IQyTQ_IJ zTh8ac0l7C1weT08PqyFi)UrV4@@k#%@>e%Ly}hq^`p-X(ySI*ud75omt*|6OxN>R# z;**MnqVxG#CN0fs%u<}ZeBz=ndXKj5^_}{?!Zgp|LfXx^M>oy&kLkQx8~aHmIPU1# zbt=cJub!TNVE(hK;rrhjUixq8990p>KmW^!C8BF;Am{4lnImdpKdz*Rs0%>|a&$^7m$K-En-i!xX3OivwpY*FLOz zc;m~bVQ2Q8JXr2tvF1}P!@Jg-(Kb!JIiJ6KRB_~9aohZ8X3@Wx#cw13Y`eU4&;8}I zKL)UBTn)W*ZO_()``X0v;o8CY~_= zDeitswkf-PUB(xM?c3seu1-^*neASAKku&bUuWz1d0}sJ{;~fs+G!N?mGAJH#TT5~ zruJwpbH9}Pdb?e9Q+p-%%8whTd0Bjs{it5_^Ht#Yy8WMKST0JHvGg{1GvVu7!z0`F z3w`U@W8S$@ck$=+3#Wc>>+x;g)H*Hn!P4;P7z;V=l`Es~?tP!Kor@A4XjYuki=|4j+tpR#yzx$c(MtH%Q$)GWT= z{(nzFil=pdPPxSCiEqsonXoBwRtV{TK6(C}^JKTp{Z`R+&eE4pci%o~GClm zer5X6ht^m(xvES&ecI^Ep-$aLIWL|XCxpHIzkc=R+mjw2n^?e*61H8XI{o|K`9;U2 zR<54J@J)#+C3C&a@nW6Q@YG2^I)6Bw`7QG8%7HGLb8o54i(@Vi@j=C(vvuNI9~ zORrQ)bw0fy?rN0f@?fF86}AA_ZS<$MU1`jk0CKiJJ^?~lA&zk2ToFJG7} zHu>|LQ_J#InMF5$2)5@o&felXW!mAY8`GY1>;Ii+HgifZe;(V;{*%w5Kk#OUzU#g$ zyq`Px;f*_?H^PjbFJHKi@gt|hN3FNZ8$0eV{_+0rvRRckR&F(uTi>^8XRB|%LEl-) zCsyVU6}QZ5Jy9KVdz#;NKl#$R4Z1~Aw|>X33yXd+Ev_iqqV1!|rVGt$gwHy!Sx`9T z;!4(=drD32IXAL@dgds+S?JA-jjPV?TONIEtp)eY+(i}YlSQ@9&XzpbdPv_OBdIle zkCx$z>t#OQ|LwP(XX&ZFeP!5*a@}Pnm*UJ`J}gYAxp2&s{d~&)t!e9)7{6?mYN+`T zn=00nEb{fopKjTI24|v~m3fygD}Hg6JN~=*9$r6hwLnq-MwykZMr%atAxncF;`6{#D`9IQnu$^mDK}+zS3r9BmW4*vAa%^UU zG_%C3gG)Kps$cWYzm+-ldCN+3YY{EO$%+&A{Ql{Bo@dV!(~f<12NqpVIr5_6__1w1 zlMTXzkLLL=57xc9nR!#WtoQE^m113g@{J5n8`>J~%Hzn1*^|vCkL!^kW}lxgsB#B`o=@_#z~9?cB2>K{t;?27?U+o}_-KAd1#cyI2GX+>N?(>X-m@jv*N*zx|2M}~x2 z>z7~L6@QqHZBDRn{KNFB`?t6ole*OR|3GSnKi+1scTY}{^hfj05S2fN)@nuvn2RV-k!0b=x)HB9vP=u9%V1WtjZP6`zO?jeaebZ zf8AQh*w1=KJ|XERe?pBRo7%0k5BK(6i?&@fWvcAqcJrrQjGLF8nICl5Z)I~8W4>%r z&DZJr9G6(1#2?tvbM4*AC5Eq)w?6ilHOv1o=Z8eAJm;TaUupXlQ-qGYYb%$d-!q@*=Roz<*o+}h^1KsjAw&6I5kS-Z^I`uuZkTpvh0h&!yl z-X%s;uKrzFP3DHHP5k>cSf$;15B=f(Fu$Sxr7DNj`bP(q|9@@%va#TJS|3N=!=u3| zY54&!?|&)1by)6b^v^_giS_yU`iG@=3wM9}Yq@0O`AZ$4Q|+r-UI;m=NHEz-X!rYf znN2IaQ82wwI-INe*5(G*E3BK3w2Rj9uy>c7TIaLxf8RvUw~AV^qWfOG{g4~8az@Nq zy|4cTC1!ls`k=YL|Fv9Bo?lebD*JPOuYD$e*!=mLgVFP#s`BL9-coH@%$NoyNaaFnu zr;cIqf(-_f_>X+oa#8)aQj>FP?vo8y?lf?pXgC<2y*sg?O0$3Z6*az0FRhp#Zi}X{ zF4RmCE;Z5K5_B&|QFG?51g_AVGkT_2KTj*3lq_C<@8}dih7~(fbQZlYJ79WeNjgL=6pEP z$vmyhoN>y@SB*j2b#nH9yvFEr?E-t|-I!0eq%0mDF1_NYP+PYXC9>;%_|qB13Jo434r!*D;t^JK_U3Cp&bnO;}=o*dKNWLv%U{+R|v{f%X8@AJbw zBaiMbaoSydcYW&Cw~=>r&9u9P57ld4csZScOSa;K{pM-4LZy(Y}l>lU&k3$ zUzOt%V{!0oW`5_wN8;Nqq+}kLbTTvY*#Xhcr;}e_Q_D`X7jU|nbHBD)bz>;A^@&3} z&aHitel0ZT>oM=$jajObwypO#q+s#$QRrolIQ^q9pJ{$FvU=Xomobfz$$MEw(j(2C z6G~;G{VOhVTgKX7xN&XqrQNF+pS`J6RwnTMF^{{ZGbf*M0L#TSyzcoct5n6q zx!WW=FMSO;Tz+Qa%tR}{nrh>YXcnXB+Qp?g8+YHV5lUqcbLxEfXU>X#qpc5;WK{Kig_2Z?QJs9`x*E3#R{ge}!kihuG0ed5YK0J-EwXp3R@&rDMyt^r?^hNA^a(ih!+C{9IOR zzCSMh;bEtBE`LIfrlpSJ(@7Q9QahJbeKq2;f8jiVEig}J3Zrx6QK`=Ur|jnEj3z}} zsMe-!6?x*LXx(x6sOY_fr7d!&w!T--&rROFdvdX8{-UVCGjdwlCPSW_5x*>u-i6v7m&E!v{7Tni6sKz`g1x2U-uTmYL{z z_TRT#%oBz6O7*wTaT7cycCSTl#rBG7{S*%)yD#r`N{i;cm|4diocq9z@y@@7*iP0> zz3b+lxMZI!`kZ?@4yJqiJkWZ_Z|o+scqMs zH7jpfBeP*g&GE$z`E0va%zS<}WXJ6(nQa0^D;d`w=HfTr)3kr}+M}!X$7J1n$CH!% zIB50TgHtsQUR$mFDk5~3dFz(%H}6#MP*bv9+}?HMoUfH-SI~!5201!D#)1=`YTzZ!$CVD;fJQytjT@r{(h(U7u37wPI)&~!q3OLUj5X9e@+k8UoD6aSv0*#;L?|4=Uw>Jzx|!PSdMLf zOjB@TvgusS#T}_PZXKG&)$IOI@BnjXxWT2pueYz*DVQb6{O~}>gn10^ygh8$+7Fk! zKYQ5_oULwB*d#T%wfygi+_ zNMX_{)%HjAi|15zdkxq>tk=;|SF77PDJNW@aaChTix)>~J7bXR!H#Wp-cGO2^IX0)_2bI7 zw-@*E|Ij!*{Z`N8bKqZc?Vb~KB7$?vYRA(>+?TW_w#mub)TlVbguiTu}(1KsNnNG;3joyT9B_viPv;vwT}gdxmd4p zmE@Kw7f!U^QfN`akbBYFR5@}*xMrue`|p>WbKX4O>%^I`>87RoQAxhpOD33qpLjiU zx}a_V=ep&pzMHf(*K^CecKzO5+j;Wb@eLacEP^xHI?5eNFG+=c`Z}{Cs>kX1-00XN zS8iWOwYM(ilF6Tt@HWG=L-b8pm#Oa+m0*ilVSCRU4kx}d(mV1F|B79a{%Bw9+Mq>x zv*fFJe8u_HDLtu;uQz0*_ncz5mI5Bp~> zxL&kNPAt?%R_0{xQ>C`!GxJ_b9h<5CEW=~VRk>9O89aA>S1T(X-&3}=e%}WxGr4ls z6ps&yERJn+44ku{82)Jf`sq;Hu?1-#rb_&0x+dKU7FxmG#jI zeZ7I#B=UjigpK~IoEVxzzZi7sIe9&n*;Szuan?~QYsKEroO4t>o%c1ki@m$fsI|ps zc2kP%&RDfd)X6AX2hI(B5}=d?YAV(sE66p4Oh7c z`k%2obZW=@wgY^;x3}^^BnLeu?*MXG~NUDDb;5A>!2aSc5a?l)tF3 z9+{~lHUG(_U-y|MIrsB^O#LvOqri8Ki{Qy1cfY;Ni9Sa8f)>k{mAdRbBHt}z-Xv6K zKPlMQ*4}Jmpt)>|I4{ z6Xt&46`iNIVaXPgxs8c#Iq_xEfQ{y#{q|Y98f4J??Y4xNPpXRry9A!P@ z{;-azj{iZb#>+1eJACgQeDr(wr?yiy$v3=Ld|P$r*k!TF@BXZQXnlUYTG@@{ow-ki z#qHKbIkw~{U;oT7_t^JIu5Z(ho0xuGTy^4sa)f$?!s`ODh$kB#l%9Dk5ttNxb>-&Z zn@nFd_SkW+-+z;>-_dW=EUpu?yXk*zz^53eRZ>o@Dy-$)#Y0-#-i_bz?S~<}RDynL7F8k~JR> zBifUZc;Zsy*`;ofE7|{vF<0Bv=u`x^jA26h5Xif-7_;Nu0m6NOI zw-ad@kFKto78!l~;(2+Q&fWcmBKtPIOxC$%^?dvG@`Vqh&K#QB_dBU5ZrK*bw;KWt znqTMX9KHVAS7WpFhCLpiro8=CcE5X_iC)U88ug;73Omm5OloFlx4e=l5w2>-d&aG1 zj+NlW&ApFAuQc7bDK~x6PuJyZ*-tGywu>dpuIrA9v`WK+!xQVp=gn_@?rbca$tRZ= z!QcE>)$F3ULEZ(g?dB_-p7`1_D6YC5u4%fM=P9%EYR}s@4%dEozkNQJsI_{-O-&=S zOM9L_aJczMQ~Btr)i0LrK0D?4V%P3>9v+kCYF%>*G0J*8GFljKbddRsN7K9x1GGaXVZTVx1RW5$AmR?ct8}lk8bi_setEmq|36)fG3}Zp$%vqheU#IrXA?C@J2c_YygI-HFONMsJh|h`=hEV{)Bi3#{_VdzM{;G>a7EZyn7!jy3QzpjvF)O+Cr5zkkB549(tcERN?qepmadTyxeyV1 zWyxY?wiS7g(#}Pl75U`3N^a9Z!Oi`T=KqhKWN`H9llab$rv?8reDisdYkgY3)aLqx z?;o3EC;Ntf^|Q8XGW@D8y}~SV?j8=!<$)g_CRwiBCXwX1?19UEvHph|6Y~xqJs3LA z@00G!qN^$@ctSgHwoF;lm|G@5B6JTRDp| z(+^zP>b=vh`?LW6f_)dQ&R=X?^ZeC<=O#z|o&UWNZ5H#5JCnbw=CxH?OB({eML zuJd}C48z}I-v{=YKct>WDq2E)Lf zKRx!|ysIwX^Xpt;neA1lS+AHx*ChV2%X8c*yKdvUM4gx7A_l>te=g#r* z%{3J^2xqWAyWx}X`T2XFU;m_T+54KGIm+5-PK5P`dw=&ibLenb$ff&wPS!Yh=HfTa zjk#Ud1omeue!HdQ$}9JLZrdGEeWxlq>Ps-M z_=~F>zkO2uz#nQ^_QNzNb%_aMX6Mrb#SH$;Pd{Ii_9`=Yr>by0aO=<3xkm#QZfwxI zbm8R9K%3PwJrBS9vvucg4!85|adVsvGqXCbq?`z=W1O+SdCr~!r`hHzt2n|x|eh*zDQ+`9mB4=jZMmLJ8}=a(SMWKI-OT*q6&xX)ki){+rL#vn;ib6vZ_z>BEy?I zdsp(b+&8MY_xa7w*d^}UCeQlfeeG=@@2(Z>0oz3mMRl7`Hdg6)nr=Hq%xJ0Z<&4w0 z{?Wg_D5d!XuU;ej@_N?qpYn@WHo7re&)k-gtT=T=aAS+)qMK7rPFh-W%J$at>HE&k zH<7T+^*Pr>bswW&u^8C8$_u5daIcE;kafH_E zU*YcXI`U}Qn&UUm*ybrkoMb(+m+`-4h{G+rJEvG<%{FyZt@tF>s-DsR`pNl8E0-P% zsA)AQH$6A;Zl{`B`owDwZvJXL7wola&Y_Um9{cVYZk=*wzJ+xEz15#v-#k=`6zEH5 zd!|2Uul28e!D)IMt9jF&*;*)GTAJdu!QpOha<0Ya*)I;3aoOfE$4M=8y%qL+QSi2p za{CrIdJA|nKbe+PzN5AA5bLY&rVk2iSvI?TH#$<@{d_RKG~shhIP5t^uyf#`g@+Jtz%1A(>ZI~S7FuNX45`3OnJ$kDV4L{YRN-m z(Kge2GdfRs7M%^dwR7uZQ{H(8FBh~oeHYohg~RVplftZ=^Gl3mc@53fDm1F=GJc$O z4OVat`uL>uz}9CE`KP}AS9EH1T%60ycXF<~YkH@-Wi>6&+CL@yOu$o@R^yLG9aBq% zJ#KC-5?iU<7IiqXr(O*Y-- z{KF=i`rGfoG+Eo$%|DA0uBm!YnyYo=sh4E=vAMr;1hVC*x=C zHff2r?^I8)jB(QZEC8oiREvR17)X=?GE! z(aq}(-plSiRzEcE+S-_11x`YkKA zt-?FBZmmCg^yBsy!PaswR=rKr3qDhmE?NHXecy)6B?po=7z@qM+p$;V=Apm4*FTBw z=iI5V!Zm<*QuN1TGn=fF%ic~ru{z%6zIQ|K-0+5!r$0YMp4yR8H4CMw0&-ToHKyq7Y2PWF!fr;|4)W>E_x!+nPT zVJGgle$;-r{!pzpL*Vh`MSBZG^7ftDQcxJWmw9E$*?U@F-tDfAIPA%r&2d?hY2yvf zwF?<{Wr?$KH6CHv@79;I+(kY3mXmn*hxT7<)^Bg+x$DC{)g;D`GeRo!{PdX-xBsv` zU|!)}5vD%nk@M}t0w+3m?k}=me(dSmcH^~or`qRKJ1^fJel+FFtz9P~Z)8p9`D2;l znR8{zqK8q(&N2NAh*`FWvDnJ+gqa_sMZud}%bw_X$R^Kk%U1hTeXw5j&3u*%cI+#KQxfr;Pi@#@wzpSj zv3^OB``g|{&l_E;HI7P`*tcdZD#_DLv5X7dai-Aw(6jp%H#+l=s6{Q$di8sb>6C!u zt4+eo%_jAwX8-bMJihYHgrpzmH@+)Lda^l{`I2$Pwj8q(0&+f$>ZCf z_gJ9g%Ko4+7irQjbsD^DdFIOLOWo*57dw;g9~!+z{Ux(>Z<0s%*YB6MgQw zDZCbX8(q1p%QLxX#@n+}{#BL@zgaS5^5;#vv?k#2hBXEJmo{-XzlkelH(k-~|JJ%K z;?K85qB(1h)K@;7&v<7g%VfUTSFKN3e=I*-&+zZ$%X@pQ?sn&AtF;GYwpiDnTDASztw*Pq7 zoJ?)cK9S;`#s7CR!||Y`Z&oog&bCkd6h3W7Xy0>_Z(CzorYf4;;M~}>$+~OChov>l zzGm`n>Sy<-FFIAoZ`a^0sPjYmgTK)6wv#oBe*V&#)}FljXL<21!|DkSw@vUky4Etq zZfj#edO>dA>A>hOcOE)R)<11EouU`DaL)^Yy;F|YvmKqcu`A?Ksl&9pg3E4;^_@F* z_=lK%kFCdsW4+obk~fUm^WM6goU`CnTFr$Q;zq7Z!d93U6@(&f$NRPw0YYu6uqnBSh8utot*Wb zDkd&qj#+QAZd3T;EYlT19_&xvYTUdK`;*<7Z`s35-+kZJT-teQ9&`Joi%H(h<=Zlw z*F?+SoKSqC>Gpxb+k84w&5xMeADv4OZ+x(AEsvNvv&5EFOH*IwZSY(DAH2 z8&|$PBU;6GT)5lP&98O+RAuJdH(s5(y4j>>Husu9=F35v3)mOguz#>@)K9P8Z8vA# zD@R=qtF@v0Z`lGSZCfn7cO~=Zq}LYPkGIRe?poK#w8FDxpQvnj69170=k_I}YiTs? z2!D8Z@~7`7&pa;b+b#V%a+-Cwq{f|Fr?yG!HV3