Implement instanced groundcover

pull/3023/head
Andrei Kortunov 5 years ago
parent f917037ead
commit 14cf0ce1dc

@ -21,7 +21,7 @@ add_openmw_dir (mwrender
actors objects renderingmanager animation rotatecontroller sky npcanimation vismask
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager
bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
)
add_openmw_dir (mwinput

@ -460,6 +460,11 @@ void OMW::Engine::addContentFile(const std::string& file)
mContentFiles.push_back(file);
}
void OMW::Engine::addGroundcoverFile(const std::string& file)
{
mGroundcoverFiles.emplace_back(file);
}
void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame)
{
mSkipMenu = skipMenu;
@ -720,8 +725,8 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
}
// Create the world
mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(),
mFileCollections, mContentFiles, mEncoder, mActivationDistanceOverride, mCellName,
mEnvironment.setWorld(new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(),
mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder, mActivationDistanceOverride, mCellName,
mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string()));
mEnvironment.getWorld()->setupPlayer();

@ -85,6 +85,7 @@ namespace OMW
osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation;
std::string mCellName;
std::vector<std::string> mContentFiles;
std::vector<std::string> mGroundcoverFiles;
bool mSkipMenu;
bool mUseSound;
bool mCompileAll;
@ -155,6 +156,7 @@ namespace OMW
* @param file - filename (extension is required)
*/
void addContentFile(const std::string& file);
void addGroundcoverFile(const std::string& file);
/// Disable or enable all sounds
void setSoundUsage(bool soundUsage);

@ -62,6 +62,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
("content", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon")
("groundcover", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon")
("no-sound", bpo::value<bool>()->implicit_value(true)
->default_value(false), "disable all sounds")
@ -190,11 +193,15 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
return false;
}
StringsVector::const_iterator it(content.begin());
StringsVector::const_iterator end(content.end());
for (; it != end; ++it)
for (auto& file : content)
{
engine.addContentFile(file);
}
StringsVector groundcover = variables["groundcover"].as<Files::EscapeStringVector>().toStdStringVector();
for (auto& file : groundcover)
{
engine.addContentFile(*it);
engine.addGroundcoverFile(file);
}
// startup-settings

@ -269,7 +269,7 @@ namespace MWGui
mWaterTextureSize->setIndexSelected(2);
int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water");
waterReflectionDetail = std::min(4, std::max(0, waterReflectionDetail));
waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail));
mWaterReflectionDetail->setIndexSelected(waterReflectionDetail);
mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video"));
@ -353,7 +353,7 @@ namespace MWGui
void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos)
{
unsigned int level = std::min((unsigned int)4, (unsigned int)pos);
unsigned int level = std::min((unsigned int)5, (unsigned int)pos);
Settings::Manager::setInt("reflection detail", "Water", level);
apply();
}

@ -0,0 +1,264 @@
#include "groundcover.hpp"
#include <osg/Geometry>
#include <osg/VertexAttribDivisor>
#include <components/esm/esmreader.hpp>
#include "apps/openmw/mwworld/esmstore.hpp"
#include "apps/openmw/mwbase/environment.hpp"
#include "apps/openmw/mwbase/world.hpp"
#include "vismask.hpp"
namespace MWRender
{
void GroundcoverUpdater::setWindSpeed(float windSpeed)
{
mWindSpeed = windSpeed;
}
void GroundcoverUpdater::setPlayerPos(osg::Vec3f playerPos)
{
mPlayerPos = playerPos;
}
void GroundcoverUpdater::setDefaults(osg::StateSet *stateset)
{
osg::ref_ptr<osg::Uniform> windUniform = new osg::Uniform("windSpeed", 0.0f);
stateset->addUniform(windUniform.get());
osg::ref_ptr<osg::Uniform> playerPosUniform = new osg::Uniform("playerPos", osg::Vec3f(0.f, 0.f, 0.f));
stateset->addUniform(playerPosUniform.get());
}
void GroundcoverUpdater::apply(osg::StateSet *stateset, osg::NodeVisitor *nv)
{
osg::ref_ptr<osg::Uniform> windUniform = stateset->getUniform("windSpeed");
if (windUniform != nullptr)
windUniform->set(mWindSpeed);
osg::ref_ptr<osg::Uniform> playerPosUniform = stateset->getUniform("playerPos");
if (playerPosUniform != nullptr)
playerPosUniform->set(mPlayerPos);
}
class InstancingVisitor : public osg::NodeVisitor
{
public:
InstancingVisitor(std::vector<Groundcover::GroundcoverEntry>& instances, osg::Vec3f& chunkPosition)
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
, mInstances(instances)
, mChunkPosition(chunkPosition)
{
}
void apply(osg::Node& node) override
{
osg::ref_ptr<osg::StateSet> ss = node.getStateSet();
if (ss != nullptr)
{
removeAlpha(ss);
}
traverse(node);
}
void apply(osg::Geometry& geom) override
{
for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i)
{
geom.getPrimitiveSet(i)->setNumInstances(mInstances.size());
}
osg::ref_ptr<osg::Vec4Array> transforms = new osg::Vec4Array(mInstances.size());
osg::BoundingBox box;
float radius = geom.getBoundingBox().radius();
for (unsigned int i = 0; i < transforms->getNumElements(); i++)
{
osg::Vec3f pos(mInstances[i].mPos.asVec3());
osg::Vec3f relativePos = pos - mChunkPosition;
(*transforms)[i] = osg::Vec4f(relativePos, mInstances[i].mScale);
// Use an additional margin due to groundcover animation
float instanceRadius = radius * mInstances[i].mScale * 1.1f;
osg::BoundingSphere instanceBounds(relativePos, instanceRadius);
box.expandBy(instanceBounds);
}
geom.setInitialBound(box);
osg::ref_ptr<osg::Vec3Array> rotations = new osg::Vec3Array(mInstances.size());
for (unsigned int i = 0; i < rotations->getNumElements(); i++)
{
(*rotations)[i] = mInstances[i].mPos.asRotationVec3();
}
geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX);
geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX);
osg::ref_ptr<osg::StateSet> ss = geom.getOrCreateStateSet();
ss->setAttribute(new osg::VertexAttribDivisor(6, 1));
ss->setAttribute(new osg::VertexAttribDivisor(7, 1));
removeAlpha(ss);
traverse(geom);
}
private:
std::vector<Groundcover::GroundcoverEntry> mInstances;
osg::Vec3f mChunkPosition;
void removeAlpha(osg::StateSet* stateset)
{
// MGE uses default alpha settings for groundcover, so we can not rely on alpha properties
stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC);
stateset->removeMode(GL_ALPHA_TEST);
stateset->removeAttribute(osg::StateAttribute::BLENDFUNC);
stateset->removeMode(GL_BLEND);
stateset->setRenderBinToInherit();
}
};
class DensityCalculator
{
public:
DensityCalculator(float density)
: mDensity(density)
{
}
bool isInstanceEnabled()
{
if (mDensity >= 1.f) return true;
mCurrentGroundcover += mDensity;
if (mCurrentGroundcover < 1.f) return false;
mCurrentGroundcover -= 1.f;
return true;
}
void reset() { mCurrentGroundcover = 0.f; }
private:
float mCurrentGroundcover = 0.f;
float mDensity = 0.f;
};
inline bool isInChunkBorders(ESM::CellRef& ref, osg::Vec2f& minBound, osg::Vec2f& maxBound)
{
osg::Vec2f size = maxBound - minBound;
if (size.x() >=1 && size.y() >=1) return true;
osg::Vec3f pos = ref.mPos.asVec3();
osg::Vec3f cellPos = pos / ESM::Land::REAL_SIZE;
if ((minBound.x() > std::floor(minBound.x()) && cellPos.x() < minBound.x()) || (minBound.y() > std::floor(minBound.y()) && cellPos.y() < minBound.y())
|| (maxBound.x() < std::ceil(maxBound.x()) && cellPos.x() >= maxBound.x()) || (minBound.y() < std::ceil(maxBound.y()) && cellPos.y() >= maxBound.y()))
return false;
return true;
}
osg::ref_ptr<osg::Node> Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
{
ChunkId id = std::make_tuple(center, size, activeGrid);
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(id);
if (obj)
return obj->asNode();
else
{
InstanceMap instances;
collectInstances(instances, size, center);
osg::ref_ptr<osg::Node> node = createChunk(instances, center);
mCache->addEntryToObjectCache(id, node.get());
return node;
}
}
Groundcover::Groundcover(Resource::SceneManager* sceneManager, float density)
: GenericResourceManager<ChunkId>(nullptr)
, mSceneManager(sceneManager)
, mDensity(density)
{
}
void Groundcover::collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center)
{
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
osg::Vec2f minBound = (center - osg::Vec2f(size/2.f, size/2.f));
osg::Vec2f maxBound = (center + osg::Vec2f(size/2.f, size/2.f));
DensityCalculator calculator(mDensity);
std::vector<ESM::ESMReader> esm;
osg::Vec2i startCell = osg::Vec2i(std::floor(center.x() - size/2.f), std::floor(center.y() - size/2.f));
for (int cellX = startCell.x(); cellX < startCell.x() + size; ++cellX)
{
for (int cellY = startCell.y(); cellY < startCell.y() + size; ++cellY)
{
const ESM::Cell* cell = store.get<ESM::Cell>().searchStatic(cellX, cellY);
if (!cell) continue;
calculator.reset();
for (size_t i=0; i<cell->mContextList.size(); ++i)
{
unsigned int index = cell->mContextList.at(i).index;
if (esm.size() <= index)
esm.resize(index+1);
cell->restore(esm[index], i);
ESM::CellRef ref;
ref.mRefNum.mContentFile = ESM::RefNum::RefNum_NoContentFile;
bool deleted = false;
while(cell->getNextRef(esm[index], ref, deleted))
{
if (deleted) continue;
Misc::StringUtils::lowerCaseInPlace(ref.mRefID);
std::string model;
if (!store.isGroundcover(ref.mRefID, model)) continue;
if (model.empty()) continue;
if (!calculator.isInstanceEnabled()) continue;
if (!isInChunkBorders(ref, minBound, maxBound)) continue;
model = "meshes/" + model;
instances[model].emplace_back(ref, model);
}
}
}
}
}
osg::ref_ptr<osg::Node> Groundcover::createChunk(InstanceMap& instances, const osg::Vec2f& center)
{
osg::ref_ptr<osg::Group> group = new osg::Group;
osg::Vec3f worldCenter = osg::Vec3f(center.x(), center.y(), 0)*ESM::Land::REAL_SIZE;
for (auto& pair : instances)
{
const osg::Node* temp = mSceneManager->getTemplate(pair.first);
osg::ref_ptr<osg::Node> node = static_cast<osg::Node*>(temp->clone(osg::CopyOp::DEEP_COPY_ALL&(~osg::CopyOp::DEEP_COPY_TEXTURES)));
// Keep link to original mesh to keep it in cache
group->getOrCreateUserDataContainer()->addUserObject(new Resource::TemplateRef(temp));
InstancingVisitor visitor(pair.second, worldCenter);
node->accept(visitor);
group->addChild(node);
}
group->getBound();
group->setNodeMask(Mask_Groundcover);
mSceneManager->recreateShaders(group, "groundcover", false, true);
return group;
}
unsigned int Groundcover::getNodeMask()
{
return Mask_Groundcover;
}
void Groundcover::reportStats(unsigned int frameNumber, osg::Stats *stats) const
{
stats->setAttribute(frameNumber, "Groundcover Chunk", mCache->getCacheSize());
}
}

@ -0,0 +1,69 @@
#ifndef OPENMW_MWRENDER_GROUNDCOVER_H
#define OPENMW_MWRENDER_GROUNDCOVER_H
#include <components/terrain/quadtreeworld.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/sceneutil/statesetupdater.hpp>
#include <components/esm/loadcell.hpp>
namespace MWRender
{
class GroundcoverUpdater : public SceneUtil::StateSetUpdater
{
public:
GroundcoverUpdater()
: mWindSpeed(0.f)
, mPlayerPos(osg::Vec3f())
{
}
void setWindSpeed(float windSpeed);
void setPlayerPos(osg::Vec3f playerPos);
protected:
void setDefaults(osg::StateSet *stateset) override;
void apply(osg::StateSet *stateset, osg::NodeVisitor *nv) override;
private:
float mWindSpeed;
osg::Vec3f mPlayerPos;
};
typedef std::tuple<osg::Vec2f, float, bool> ChunkId; // Center, Size, ActiveGrid
class Groundcover : public Resource::GenericResourceManager<ChunkId>, public Terrain::QuadTreeWorld::ChunkManager
{
public:
Groundcover(Resource::SceneManager* sceneManager, float density);
~Groundcover() = default;
osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override;
unsigned int getNodeMask() override;
void reportStats(unsigned int frameNumber, osg::Stats* stats) const override;
struct GroundcoverEntry
{
ESM::Position mPos;
float mScale;
std::string mModel;
GroundcoverEntry(const ESM::CellRef& ref, const std::string& model)
{
mPos = ref.mPos;
mScale = ref.mScale;
mModel = model;
}
};
private:
Resource::SceneManager* mSceneManager;
float mDensity;
typedef std::map<std::string, std::vector<GroundcoverEntry>> InstanceMap;
osg::ref_ptr<osg::Node> createChunk(InstanceMap& instances, const osg::Vec2f& center);
void collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center);
};
}
#endif

@ -398,6 +398,7 @@ namespace MWRender
int type = store.findStatic(ref.mRefID);
if (!typeFilter(type,size>=2)) continue;
if (deleted) { refs.erase(ref.mRefNum); continue; }
if (store.isGroundcover(ref.mRefID)) continue;
refs[ref.mRefNum] = ref;
}
}

@ -3,6 +3,7 @@
#include <limits>
#include <cstdlib>
#include <osg/AlphaFunc>
#include <osg/Light>
#include <osg/LightModel>
#include <osg/Fog>
@ -68,10 +69,10 @@
#include "fogmanager.hpp"
#include "objectpaging.hpp"
#include "screenshotmanager.hpp"
#include "groundcover.hpp"
namespace MWRender
{
class StateUpdater : public SceneUtil::StateSetUpdater
{
public:
@ -243,6 +244,10 @@ namespace MWRender
globalDefines["preLightEnv"] = Settings::Manager::getBool("apply lighting to environment maps", "Shaders") ? "1" : "0";
globalDefines["radialFog"] = Settings::Manager::getBool("radial fog", "Shaders") ? "1" : "0";
float groundcoverDistance = (Constants::CellSizeInUnits * Settings::Manager::getInt("distance", "Groundcover") - 1024) * 0.93;
globalDefines["groundcoverFadeStart"] = std::to_string(groundcoverDistance * Settings::Manager::getFloat("fade start", "Groundcover"));
globalDefines["groundcoverFadeEnd"] = std::to_string(groundcoverDistance);
// It is unnecessary to stop/start the viewer as no frames are being rendered yet.
mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(globalDefines);
@ -269,7 +274,8 @@ namespace MWRender
const bool useTerrainNormalMaps = Settings::Manager::getBool("auto use terrain normal maps", "Shaders");
const bool useTerrainSpecularMaps = Settings::Manager::getBool("auto use terrain specular maps", "Shaders");
mTerrainStorage = new TerrainStorage(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps);
mTerrainStorage.reset(new TerrainStorage(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps));
const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain");
if (Settings::Manager::getBool("distant terrain", "Terrain"))
{
@ -277,12 +283,11 @@ namespace MWRender
int compMapPower = Settings::Manager::getInt("composite map level", "Terrain");
compMapPower = std::max(-3, compMapPower);
float compMapLevel = pow(2, compMapPower);
const float lodFactor = Settings::Manager::getFloat("lod factor", "Terrain");
const int vertexLodMod = Settings::Manager::getInt("vertex lod mod", "Terrain");
float maxCompGeometrySize = Settings::Manager::getFloat("max composite geometry size", "Terrain");
maxCompGeometrySize = std::max(maxCompGeometrySize, 1.f);
mTerrain.reset(new Terrain::QuadTreeWorld(
sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug,
sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug,
compMapResolution, compMapLevel, lodFactor, vertexLodMod, maxCompGeometrySize));
if (Settings::Manager::getBool("object paging", "Terrain"))
{
@ -292,11 +297,43 @@ namespace MWRender
}
}
else
mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug));
mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage.get(), Mask_Terrain, Mask_PreCompile, Mask_Debug));
mTerrain->setTargetFrameRate(Settings::Manager::getFloat("target framerate", "Cells"));
mTerrain->setWorkQueue(mWorkQueue.get());
if (Settings::Manager::getBool("enabled", "Groundcover"))
{
osg::ref_ptr<osg::Group> groundcoverRoot = new osg::Group;
groundcoverRoot->setNodeMask(Mask_Groundcover);
groundcoverRoot->setName("Groundcover Root");
sceneRoot->addChild(groundcoverRoot);
// Force a unified alpha handling instead of data from meshes
osg::ref_ptr<osg::AlphaFunc> alpha = new osg::AlphaFunc(osg::AlphaFunc::GEQUAL, 128.f/255.f);
groundcoverRoot->getOrCreateStateSet()->setAttributeAndModes(alpha.get(), osg::StateAttribute::ON);
mGroundcoverUpdater = new GroundcoverUpdater;
groundcoverRoot->addUpdateCallback(mGroundcoverUpdater);
float chunkSize = Settings::Manager::getFloat("min chunk size", "Groundcover");
if (chunkSize >= 1.0f)
chunkSize = 1.0f;
else if (chunkSize >= 0.5f)
chunkSize = 0.5f;
else if (chunkSize >= 0.25f)
chunkSize = 0.25f;
else if (chunkSize != 0.125f)
chunkSize = 0.125f;
float density = Settings::Manager::getFloat("density", "Groundcover");
density = std::clamp(density, 0.f, 1.f);
mGroundcoverWorld.reset(new Terrain::QuadTreeWorld(groundcoverRoot, mTerrainStorage.get(), Mask_Groundcover, lodFactor, chunkSize));
mGroundcover.reset(new Groundcover(mResourceSystem->getSceneManager(), density));
static_cast<Terrain::QuadTreeWorld*>(mGroundcoverWorld.get())->addChunkManager(mGroundcover.get());
mResourceSystem->addResourceManager(mGroundcover.get());
}
// water goes after terrain for correct waterculling order
mWater.reset(new Water(sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation(), resourcePath));
@ -508,7 +545,11 @@ namespace MWRender
mWater->changeCell(store);
if (store->getCell()->isExterior())
{
mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY());
if (mGroundcoverWorld)
mGroundcoverWorld->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY());
}
}
void RenderingManager::removeCell(const MWWorld::CellStore *store)
{
@ -517,7 +558,11 @@ namespace MWRender
mObjects->removeCell(store);
if (store->getCell()->isExterior())
{
mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY());
if (mGroundcoverWorld)
mGroundcoverWorld->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY());
}
mWater->removeCell(store);
}
@ -527,6 +572,8 @@ namespace MWRender
if (!enable)
mWater->setCullCallback(nullptr);
mTerrain->enable(enable);
if (mGroundcoverWorld)
mGroundcoverWorld->enable(enable);
}
void RenderingManager::setSkyEnabled(bool enabled)
@ -612,6 +659,16 @@ namespace MWRender
mEffectManager->update(dt);
mSky->update(dt);
mWater->update(dt);
if (mGroundcoverUpdater)
{
const MWWorld::Ptr& player = mPlayerAnimation->getPtr();
osg::Vec3f playerPos(player.getRefData().getPosition().asVec3());
float windSpeed = mSky->getBaseWindSpeed();
mGroundcoverUpdater->setWindSpeed(windSpeed);
mGroundcoverUpdater->setPlayerPos(playerPos);
}
}
updateNavMesh();
@ -805,7 +862,7 @@ namespace MWRender
mIntersectionVisitor->setIntersector(intersector);
int mask = ~0;
mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater);
mask &= ~(Mask_RenderToTexture|Mask_Sky|Mask_Debug|Mask_Effect|Mask_Water|Mask_SimpleWater|Mask_Groundcover);
if (ignorePlayer)
mask &= ~(Mask_Player);
if (ignoreActors)
@ -964,6 +1021,12 @@ namespace MWRender
fov = std::min(mFieldOfView, 140.f);
float distanceMult = std::cos(osg::DegreesToRadians(fov)/2.f);
mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f/distanceMult : 1.f));
if (mGroundcoverWorld)
{
int groundcoverDistance = Constants::CellSizeInUnits * Settings::Manager::getInt("distance", "Groundcover");
mGroundcoverWorld->setViewDistance(groundcoverDistance * (distanceMult ? 1.f/distanceMult : 1.f));
}
}
void RenderingManager::updateTextureFiltering()
@ -1158,6 +1221,8 @@ namespace MWRender
void RenderingManager::setActiveGrid(const osg::Vec4i &grid)
{
mTerrain->setActiveGrid(grid);
if (mGroundcoverWorld)
mGroundcoverWorld->setActiveGrid(grid);
}
bool RenderingManager::pagingEnableObject(int type, const MWWorld::ConstPtr& ptr, bool enabled)
{

@ -70,7 +70,7 @@ namespace DetourNavigator
namespace MWRender
{
class GroundcoverUpdater;
class StateUpdater;
class EffectManager;
@ -88,6 +88,7 @@ namespace MWRender
class ActorsPaths;
class RecastMesh;
class ObjectPaging;
class Groundcover;
class RenderingManager : public MWRender::RenderingInterface
{
@ -261,6 +262,8 @@ namespace MWRender
osg::ref_ptr<osg::Group> mSceneRoot;
Resource::ResourceSystem* mResourceSystem;
osg::ref_ptr<GroundcoverUpdater> mGroundcoverUpdater;
osg::ref_ptr<SceneUtil::WorkQueue> mWorkQueue;
osg::ref_ptr<SceneUtil::UnrefQueue> mUnrefQueue;
@ -275,8 +278,10 @@ namespace MWRender
std::unique_ptr<Objects> mObjects;
std::unique_ptr<Water> mWater;
std::unique_ptr<Terrain::World> mTerrain;
TerrainStorage* mTerrainStorage;
std::unique_ptr<Terrain::World> mGroundcoverWorld;
std::unique_ptr<TerrainStorage> mTerrainStorage;
std::unique_ptr<ObjectPaging> mObjectPaging;
std::unique_ptr<Groundcover> mGroundcover;
std::unique_ptr<SkyManager> mSky;
std::unique_ptr<FogManager> mFog;
std::unique_ptr<ScreenshotManager> mScreenshotManager;

@ -53,7 +53,9 @@ namespace MWRender
Mask_PreCompile = (1<<18),
// Set on a camera's cull mask to enable the LightManager
Mask_Lighting = (1<<19)
Mask_Lighting = (1<<19),
Mask_Groundcover = (1<<20),
};
}

@ -244,7 +244,7 @@ public:
setCullCallback(new InheritViewPointCallback);
setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR);
setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting);
setCullMask(Mask_Effect|Mask_Scene|Mask_Object|Mask_Static|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting|Mask_Groundcover);
setNodeMask(Mask_RenderToTexture);
setViewport(0, 0, rttSize, rttSize);
@ -372,12 +372,13 @@ public:
void setInterior(bool isInterior)
{
int reflectionDetail = Settings::Manager::getInt("reflection detail", "Water");
reflectionDetail = std::min(4, std::max(isInterior ? 2 : 0, reflectionDetail));
reflectionDetail = std::min(5, std::max(isInterior ? 2 : 0, reflectionDetail));
unsigned int extraMask = 0;
if(reflectionDetail >= 1) extraMask |= Mask_Terrain;
if(reflectionDetail >= 2) extraMask |= Mask_Static;
if(reflectionDetail >= 3) extraMask |= Mask_Effect|Mask_ParticleSystem|Mask_Object;
if(reflectionDetail >= 4) extraMask |= Mask_Player|Mask_Actor;
if(reflectionDetail >= 5) extraMask |= Mask_Groundcover;
setCullMask(Mask_Scene|Mask_Sky|Mask_Lighting|extraMask);
}

@ -36,6 +36,13 @@ namespace MWWorld
virtual bool operator()(const MWWorld::Ptr& ptr)
{
if (ptr.getTypeName()==typeid (ESM::Static).name())
{
const MWWorld::LiveCellRef<ESM::Static> *ref = ptr.get<ESM::Static>();
if (ref->mBase->mIsGroundcover)
return true;
}
ptr.getClass().getModelsToPreload(ptr, mOut);
return true;

@ -24,6 +24,8 @@ namespace MWWorld
/// all methods are known.
void load (ESM::CellRef &ref, bool deleted, const MWWorld::ESMStore &esmStore);
inline bool ignoreInstance (const X* ptr);
LiveRef &insert (const LiveRef &item)
{
mList.push_back(item);

@ -169,6 +169,17 @@ namespace
namespace MWWorld
{
template <typename X>
bool CellRefList<X>::ignoreInstance (const X* ptr)
{
return false;
}
template <>
bool CellRefList<ESM::Static>::ignoreInstance (const ESM::Static* ptr)
{
return ptr->mIsGroundcover;
}
template <typename X>
void CellRefList<X>::load(ESM::CellRef &ref, bool deleted, const MWWorld::ESMStore &esmStore)
@ -177,6 +188,8 @@ namespace MWWorld
if (const X *ptr = store.search (ref.mRefID))
{
if (ignoreInstance(ptr)) return;
typename std::list<LiveRef>::iterator iter =
std::find(mList.begin(), mList.end(), ref.mRefNum);

@ -21,7 +21,7 @@ struct ContentLoader
{
}
virtual void load(const boost::filesystem::path& filepath, int& index)
virtual void load(const boost::filesystem::path& filepath, int& index, bool isGroundcover)
{
Log(Debug::Info) << "Loading content file " << filepath.string();
mListener.setLabel(MyGUI::TextIterator::toTagsString(filepath.string()));

@ -15,17 +15,17 @@ EsmLoader::EsmLoader(MWWorld::ESMStore& store, std::vector<ESM::ESMReader>& read
{
}
void EsmLoader::load(const boost::filesystem::path& filepath, int& index)
void EsmLoader::load(const boost::filesystem::path& filepath, int& index, bool isGroundcover)
{
ContentLoader::load(filepath.filename(), index);
ContentLoader::load(filepath.filename(), index, isGroundcover);
ESM::ESMReader lEsm;
lEsm.setEncoder(mEncoder);
lEsm.setIndex(index);
lEsm.setGlobalReaderList(&mEsm);
lEsm.open(filepath.string());
mEsm[index] = lEsm;
mStore.load(mEsm[index], &mListener);
ESM::ESMReader lEsm;
lEsm.setEncoder(mEncoder);
lEsm.setIndex(index);
lEsm.setGlobalReaderList(&mEsm);
lEsm.open(filepath.string(), isGroundcover);
mEsm[index] = lEsm;
mStore.load(mEsm[index], &mListener);
}
} /* namespace MWWorld */

@ -25,7 +25,7 @@ struct EsmLoader : public ContentLoader
EsmLoader(MWWorld::ESMStore& store, std::vector<ESM::ESMReader>& readers,
ToUTF8::Utf8Encoder* encoder, Loading::Listener& listener);
void load(const boost::filesystem::path& filepath, int& index) override;
void load(const boost::filesystem::path& filepath, int& index, bool isGroundcover) override;
private:
std::vector<ESM::ESMReader>& mEsm;

@ -190,6 +190,15 @@ void ESMStore::setUp(bool validateRecords)
{
validate();
countRecords();
if (mGroundcovers.empty())
{
for (const ESM::Static& record : mStatics)
{
if (record.mIsGroundcover)
mGroundcovers[record.mId] = record.mModel;
}
}
}
}

@ -75,6 +75,7 @@ namespace MWWorld
// maps the id name to the record type.
std::map<std::string, int> mIds;
std::map<std::string, int> mStaticIds;
std::map<std::string, std::string> mGroundcovers;
std::map<std::string, int> mRefCount;
@ -121,6 +122,22 @@ namespace MWWorld
return it->second;
}
bool isGroundcover(const std::string &id, std::string &model) const
{
std::map<std::string, std::string>::const_iterator it = mGroundcovers.find(id);
if (it == mGroundcovers.end()) {
return false;
}
model = it->second;
return true;
}
bool isGroundcover(const std::string &id) const
{
std::map<std::string, std::string>::const_iterator it = mGroundcovers.find(id);
return (it != mGroundcovers.end());
}
ESMStore()
: mDynamicCount(0)
{

@ -103,12 +103,12 @@ namespace MWWorld
return mLoaders.insert(std::make_pair(extension, loader)).second;
}
void load(const boost::filesystem::path& filepath, int& index) override
void load(const boost::filesystem::path& filepath, int& index, bool isGroundcover) override
{
LoadersContainer::iterator it(mLoaders.find(Misc::StringUtils::lowerCase(filepath.extension().string())));
if (it != mLoaders.end())
{
it->second->load(filepath, index);
it->second->load(filepath, index, isGroundcover);
}
else
{
@ -140,6 +140,7 @@ namespace MWWorld
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Files::Collections& fileCollections,
const std::vector<std::string>& contentFiles,
const std::vector<std::string>& groundcoverFiles,
ToUTF8::Utf8Encoder* encoder, int activationDistanceOverride,
const std::string& startCell, const std::string& startupScript,
const std::string& resourcePath, const std::string& userDataPath)
@ -152,7 +153,7 @@ namespace MWWorld
mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0),
mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f)
{
mEsm.resize(contentFiles.size());
mEsm.resize(contentFiles.size() + groundcoverFiles.size());
Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
listener->loadingOn();
@ -165,7 +166,7 @@ namespace MWWorld
gameContentLoader.addLoader(".omwaddon", &esmLoader);
gameContentLoader.addLoader(".project", &esmLoader);
loadContentFiles(fileCollections, contentFiles, gameContentLoader);
loadContentFiles(fileCollections, contentFiles, groundcoverFiles, gameContentLoader);
listener->loadingOff();
@ -2941,7 +2942,7 @@ namespace MWWorld
}
void World::loadContentFiles(const Files::Collections& fileCollections,
const std::vector<std::string>& content, ContentLoader& contentLoader)
const std::vector<std::string>& content, const std::vector<std::string>& groundcover, ContentLoader& contentLoader)
{
int idx = 0;
for (const std::string &file : content)
@ -2950,7 +2951,7 @@ namespace MWWorld
const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string());
if (col.doesExist(file))
{
contentLoader.load(col.getPath(file), idx);
contentLoader.load(col.getPath(file), idx, false);
}
else
{
@ -2959,6 +2960,22 @@ namespace MWWorld
}
idx++;
}
for (const std::string &file : groundcover)
{
boost::filesystem::path filename(file);
const Files::MultiDirCollection& col = fileCollections.getCollection(filename.extension().string());
if (col.doesExist(file))
{
contentLoader.load(col.getPath(file), idx, true);
}
else
{
std::string message = "Failed loading " + file + ": the groundcover file does not exist";
throw std::runtime_error(message);
}
idx++;
}
}
bool World::startSpellCast(const Ptr &actor)

@ -177,7 +177,7 @@ namespace MWWorld
* @param contentLoader -
*/
void loadContentFiles(const Files::Collections& fileCollections,
const std::vector<std::string>& content, ContentLoader& contentLoader);
const std::vector<std::string>& content, const std::vector<std::string>& groundcover, ContentLoader& contentLoader);
float feetToGameUnits(float feet);
float getActivationDistancePlusTelekinesis();
@ -196,6 +196,7 @@ namespace MWWorld
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Files::Collections& fileCollections,
const std::vector<std::string>& contentFiles,
const std::vector<std::string>& groundcoverFiles,
ToUTF8::Utf8Encoder* encoder, int activationDistanceOverride,
const std::string& startCell, const std::string& startupScript,
const std::string& resourcePath, const std::string& userDataPath);

@ -100,6 +100,7 @@ bool Config::GameSettings::readFile(QTextStream &stream, QMultiMap<QString, QStr
if (key != QLatin1String("data")
&& key != QLatin1String("fallback-archive")
&& key != QLatin1String("content")
&& key != QLatin1String("groundcover")
&& key != QLatin1String("script-blacklist"))
settings.remove(key);
@ -200,6 +201,7 @@ bool Config::GameSettings::isOrderedLine(const QString& line)
|| line.contains(QRegExp("^\\s*data\\s*="))
|| line.contains(QRegExp("^\\s*data-local\\s*="))
|| line.contains(QRegExp("^\\s*resources\\s*="))
|| line.contains(QRegExp("^\\s*groundcover\\s*="))
|| line.contains(QRegExp("^\\s*content\\s*="));
}

@ -25,6 +25,7 @@ ESMReader::ESMReader()
, mGlobalReaderList(nullptr)
, mEncoder(nullptr)
, mFileSize(0)
, mIsGroundcoverFile(false)
{
clearCtx();
}
@ -80,8 +81,10 @@ void ESMReader::openRaw(const std::string& filename)
openRaw(Files::openConstrainedFileStream(filename.c_str()), filename);
}
void ESMReader::open(Files::IStreamPtr _esm, const std::string &name)
void ESMReader::open(Files::IStreamPtr _esm, const std::string &name, bool isGroundcover)
{
mIsGroundcoverFile = isGroundcover;
openRaw(_esm, name);
if (getRecName() != "TES3")
@ -92,9 +95,9 @@ void ESMReader::open(Files::IStreamPtr _esm, const std::string &name)
mHeader.load (*this);
}
void ESMReader::open(const std::string &file)
void ESMReader::open(const std::string &file, bool isGroundcover)
{
open (Files::openConstrainedFileStream (file.c_str ()), file);
open (Files::openConstrainedFileStream (file.c_str ()), file, isGroundcover);
}
int64_t ESMReader::getHNLong(const char *name)

@ -31,6 +31,7 @@ public:
int getVer() const { return mHeader.mData.version; }
int getRecordCount() const { return mHeader.mData.records; }
bool isGroundcoverFile() const { return mIsGroundcoverFile; }
float getFVer() const { return (mHeader.mData.version == VER_12) ? 1.2f : 1.3f; }
const std::string getAuthor() const { return mHeader.mData.author; }
const std::string getDesc() const { return mHeader.mData.desc; }
@ -66,9 +67,9 @@ public:
/// Load ES file from a new stream, parses the header. Closes the
/// currently open file first, if any.
void open(Files::IStreamPtr _esm, const std::string &name);
void open(Files::IStreamPtr _esm, const std::string &name, bool isGroundcover = false);
void open(const std::string &file);
void open(const std::string &file, bool isGroundcover = false);
void openRaw(const std::string &filename);
@ -290,6 +291,7 @@ private:
size_t mFileSize;
bool mIsGroundcoverFile;
};
}
#endif

@ -37,6 +37,8 @@ namespace ESM
if (!hasName)
esm.fail("Missing NAME subrecord");
mIsGroundcover = esm.isGroundcoverFile();
}
void Static::save(ESMWriter &esm, bool isDeleted) const
{

@ -28,6 +28,8 @@ struct Static
std::string mId, mModel;
bool mIsGroundcover = false;
void load(ESMReader &esm, bool &isDeleted);
void save(ESMWriter &esm, bool isDeleted = false) const;

@ -247,10 +247,12 @@ namespace Resource
return mForceShaders;
}
void SceneManager::recreateShaders(osg::ref_ptr<osg::Node> node, const std::string& shaderPrefix, bool translucentFramebuffer)
void SceneManager::recreateShaders(osg::ref_ptr<osg::Node> node, const std::string& shaderPrefix, bool translucentFramebuffer, bool forceShadersForNode)
{
osg::ref_ptr<Shader::ShaderVisitor> shaderVisitor(createShaderVisitor(shaderPrefix, translucentFramebuffer));
shaderVisitor->setAllowedToModifyStateSets(false);
if (forceShadersForNode)
shaderVisitor->setForceShaders(true);
node->accept(*shaderVisitor);
}
@ -512,7 +514,7 @@ namespace Resource
SetFilterSettingsControllerVisitor setFilterSettingsControllerVisitor(mMinFilter, mMagFilter, mMaxAnisotropy);
loaded->accept(setFilterSettingsControllerVisitor);
osg::ref_ptr<Shader::ShaderVisitor> shaderVisitor (createShaderVisitor());
osg::ref_ptr<Shader::ShaderVisitor> shaderVisitor (createShaderVisitor("objects"));
loaded->accept(*shaderVisitor);
// share state

@ -76,7 +76,7 @@ namespace Resource
Shader::ShaderManager& getShaderManager();
/// Re-create shaders for this node, need to call this if texture stages or vertex color mode have changed.
void recreateShaders(osg::ref_ptr<osg::Node> node, const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false);
void recreateShaders(osg::ref_ptr<osg::Node> node, const std::string& shaderPrefix = "objects", bool translucentFramebuffer = false, bool forceShadersForNode = false);
/// @see ShaderVisitor::setForceShaders
void setForceShaders(bool force);

@ -281,6 +281,7 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer)
"FrameNumber",
"",
"Compiling",
"UnrefQueue",
"WorkQueue",
"WorkThread",
"",
@ -294,14 +295,13 @@ void StatsHandler::setUpScene(osgViewer::ViewerBase *viewer)
"Nif",
"Keyframe",
"",
"Groundcover Chunk",
"Object Chunk",
"Terrain Chunk",
"Terrain Texture",
"Land",
"Composite",
"",
"UnrefQueue",
"",
"NavMesh UpdateJobs",
"NavMesh CacheSize",
"NavMesh UsedTiles",

@ -342,6 +342,8 @@ namespace Shader
osg::ref_ptr<osg::Program> program (new osg::Program);
program->addShader(vertexShader);
program->addShader(fragmentShader);
program->addBindAttribLocation("aOffset", 6);
program->addBindAttribLocation("aRotation", 7);
found = mPrograms.insert(std::make_pair(std::make_pair(vertexShader, fragmentShader), program)).first;
}
return found->second;

@ -40,7 +40,7 @@ ChunkManager::ChunkManager(Storage *storage, Resource::SceneManager *sceneMgr, T
mMultiPassRoot->setAttributeAndModes(material, osg::StateAttribute::ON);
}
osg::ref_ptr<osg::Node> ChunkManager::getChunk(float size, const osg::Vec2f &center, unsigned char lod, unsigned int lodFlags, bool far, const osg::Vec3f& viewPoint, bool compile)
osg::ref_ptr<osg::Node> ChunkManager::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile)
{
ChunkId id = std::make_tuple(center, lod, lodFlags);
osg::ref_ptr<osg::Object> obj = mCache->getRefFromObjectCache(id);

@ -35,7 +35,7 @@ namespace Terrain
public:
ChunkManager(Storage* storage, Resource::SceneManager* sceneMgr, TextureManager* textureManager, CompositeMapRenderer* renderer);
osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool far, const osg::Vec3f& viewPoint, bool compile) override;
osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) override;
void setCompositeMapSize(unsigned int size) { mCompositeMapSize = size; }
void setCompositeMapLevel(float level) { mCompositeMapLevel = level; }

@ -90,8 +90,6 @@ private:
osg::Vec4i mActiveGrid;
};
const float MIN_SIZE = 1/8.f;
class RootNode : public QuadTreeNode
{
public:
@ -250,6 +248,7 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour
, mLodFactor(lodFactor)
, mVertexLodMod(vertexLodMod)
, mViewDistance(std::numeric_limits<float>::max())
, mMinSize(1/8.f)
{
mChunkManager->setCompositeMapSize(compMapResolution);
mChunkManager->setCompositeMapLevel(compMapLevel);
@ -257,6 +256,17 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour
mChunkManagers.push_back(mChunkManager.get());
}
QuadTreeWorld::QuadTreeWorld(osg::Group *parent, Storage *storage, int nodeMask, float lodFactor, float chunkSize)
: TerrainGrid(parent, storage, nodeMask)
, mViewDataMap(new ViewDataMap)
, mQuadTreeBuilt(false)
, mLodFactor(lodFactor)
, mVertexLodMod(0)
, mViewDistance(std::numeric_limits<float>::max())
, mMinSize(chunkSize)
{
}
QuadTreeWorld::~QuadTreeWorld()
{
}
@ -425,7 +435,7 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv)
if (needsUpdate)
{
vd->reset();
DefaultLodCallback lodCallback(mLodFactor, MIN_SIZE, mViewDistance, mActiveGrid);
DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, mActiveGrid);
mRootNode->traverseNodes(vd, nv.getViewPoint(), &lodCallback);
}
@ -438,7 +448,7 @@ void QuadTreeWorld::accept(osg::NodeVisitor &nv)
entry.mRenderingNode->accept(nv);
}
if (isCullVisitor)
if (mHeightCullCallback && isCullVisitor)
updateWaterCullingView(mHeightCullCallback, vd, static_cast<osgUtil::CullVisitor*>(&nv), mStorage->getCellWorldSize(), !isGridEmpty());
vd->markUnchanged();
@ -457,7 +467,7 @@ void QuadTreeWorld::ensureQuadTreeBuilt()
if (mQuadTreeBuilt)
return;
QuadTreeBuilder builder(mStorage, MIN_SIZE);
QuadTreeBuilder builder(mStorage, mMinSize);
builder.build();
mRootNode = builder.getRootNode();
@ -491,7 +501,7 @@ void QuadTreeWorld::preload(View *view, const osg::Vec3f &viewPoint, const osg::
ViewData* vd = static_cast<ViewData*>(view);
vd->setViewPoint(viewPoint);
vd->setActiveGrid(grid);
DefaultLodCallback lodCallback(mLodFactor, MIN_SIZE, mViewDistance, grid);
DefaultLodCallback lodCallback(mLodFactor, mMinSize, mViewDistance, grid);
mRootNode->traverseNodes(vd, viewPoint, &lodCallback);
if (!progressTotal)
@ -515,14 +525,15 @@ bool QuadTreeWorld::storeView(const View* view, double referenceTime)
void QuadTreeWorld::reportStats(unsigned int frameNumber, osg::Stats *stats)
{
stats->setAttribute(frameNumber, "Composite", mCompositeMapRenderer->getCompileSetSize());
if (mCompositeMapRenderer)
stats->setAttribute(frameNumber, "Composite", mCompositeMapRenderer->getCompileSetSize());
}
void QuadTreeWorld::loadCell(int x, int y)
{
// fallback behavior only for undefined cells (every other is already handled in quadtree)
float dummy;
if (!mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy))
if (mChunkManager && !mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy))
TerrainGrid::loadCell(x,y);
else
World::loadCell(x,y);
@ -532,7 +543,7 @@ void QuadTreeWorld::unloadCell(int x, int y)
{
// fallback behavior only for undefined cells (every other is already handled in quadtree)
float dummy;
if (!mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy))
if (mChunkManager && !mStorage->getMinMaxHeights(1, osg::Vec2f(x+0.5, y+0.5), dummy, dummy))
TerrainGrid::unloadCell(x,y);
else
World::unloadCell(x,y);

@ -22,6 +22,8 @@ namespace Terrain
public:
QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask, int compMapResolution, float comMapLevel, float lodFactor, int vertexLodMod, float maxCompGeometrySize);
QuadTreeWorld(osg::Group *parent, Storage *storage, int nodeMask, float lodFactor, float chunkSize);
~QuadTreeWorld();
void accept(osg::NodeVisitor& nv);
@ -47,7 +49,7 @@ namespace Terrain
{
public:
virtual ~ChunkManager(){}
virtual osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool far, const osg::Vec3f& viewPoint, bool compile) = 0;
virtual osg::ref_ptr<osg::Node> getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) = 0;
virtual unsigned int getNodeMask() { return 0; }
};
void addChunkManager(ChunkManager*);
@ -66,6 +68,7 @@ namespace Terrain
float mLodFactor;
int mVertexLodMod;
float mViewDistance;
float mMinSize;
};
}

@ -26,6 +26,12 @@ TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::
{
}
TerrainGrid::TerrainGrid(osg::Group* parent, Storage* storage, int nodeMask)
: Terrain::World(parent, storage, nodeMask)
, mNumSplits(4)
{
}
TerrainGrid::~TerrainGrid()
{
while (!mGrid.empty())
@ -107,6 +113,8 @@ void TerrainGrid::unloadCell(int x, int y)
void TerrainGrid::updateWaterCulling()
{
if (!mHeightCullCallback) return;
osg::ComputeBoundsVisitor computeBoundsVisitor;
mTerrainRoot->accept(computeBoundsVisitor);
float lowZ = computeBoundsVisitor.getBoundingBox()._min.z();

@ -15,6 +15,7 @@ namespace Terrain
{
public:
TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0);
TerrainGrid(osg::Group* parent, Storage* storage, int nodeMask=~0);
~TerrainGrid();
void cacheCell(View* view, int x, int y) override;

@ -49,17 +49,38 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst
mResourceSystem->addResourceManager(mTextureManager.get());
}
World::World(osg::Group* parent, Storage* storage, int nodeMask)
: mStorage(storage)
, mParent(parent)
, mCompositeMapCamera(nullptr)
, mCompositeMapRenderer(nullptr)
, mResourceSystem(nullptr)
, mTextureManager(nullptr)
, mChunkManager(nullptr)
, mCellBorder(nullptr)
, mBorderVisible(false)
, mHeightCullCallback(nullptr)
{
mTerrainRoot = new osg::Group;
mTerrainRoot->setNodeMask(nodeMask);
mParent->addChild(mTerrainRoot);
}
World::~World()
{
mResourceSystem->removeResourceManager(mChunkManager.get());
mResourceSystem->removeResourceManager(mTextureManager.get());
if (mResourceSystem && mChunkManager)
mResourceSystem->removeResourceManager(mChunkManager.get());
if (mResourceSystem && mTextureManager)
mResourceSystem->removeResourceManager(mTextureManager.get());
mParent->removeChild(mTerrainRoot);
mCompositeMapCamera->removeChild(mCompositeMapRenderer);
mCompositeMapCamera->getParent(0)->removeChild(mCompositeMapCamera);
delete mStorage;
if (mCompositeMapCamera && mCompositeMapRenderer)
{
mCompositeMapCamera->removeChild(mCompositeMapRenderer);
mCompositeMapCamera->getParent(0)->removeChild(mCompositeMapCamera);
}
}
void World::setWorkQueue(SceneUtil::WorkQueue* workQueue)
@ -108,16 +129,20 @@ float World::getHeightAt(const osg::Vec3f &worldPos)
void World::updateTextureFiltering()
{
mTextureManager->updateTextureFiltering();
if (mTextureManager)
mTextureManager->updateTextureFiltering();
}
void World::clearAssociatedCaches()
{
mChunkManager->clearCache();
if (mChunkManager)
mChunkManager->clearCache();
}
osg::Callback* World::getHeightCullCallback(float highz, unsigned int mask)
{
if (!mHeightCullCallback) return nullptr;
mHeightCullCallback->setHighZ(highz);
mHeightCullCallback->setCullMask(mask);
return mHeightCullCallback;

@ -106,6 +106,7 @@ namespace Terrain
/// @param nodeMask mask for the terrain root
/// @param preCompileMask mask for pre compiling textures
World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask);
World(osg::Group* parent, Storage* storage, int nodeMask);
virtual ~World();
/// Set a WorkQueue to delete objects in the background thread.

@ -223,10 +223,10 @@ For example, to attach a custom weapon bone, you'll need to follow this NIF reco
::
NiNode "root"
NiNode "Bip01 L Hand"
NiNode "Weapon Bone Left"
NiStringExtraData "BONE"
NiNode "root"
NiNode "Bip01 L Hand"
NiNode "Weapon Bone Left"
NiStringExtraData "BONE"
OpenMW will detect ``Weapon Bone Left`` node and attach it to ``Bip01 L Hand`` bone of the target skeleton.
@ -276,6 +276,54 @@ Also it is possible to add a "Bip01 Arrow" bone to actor skeletons. In this case
Such approach allows to implement better shooting animations (for example, beast races have tail, so quivers should be attached under different angle and
default arrow fetching animation does not look good).
Groundcover support
-------------------
Groundcover objects is a special kind of objects (e.g. grass), which can be used to improve visual fidelity.
They use these assumptions:
1. Each object is independent, so part of objects can be removed from scene without causing graphical artifacts.
2. Groundover should not have collisions.
3. They are not important for some parts of game scene (e.g. local map).
4. They can not be moved or disabled on the fly.
5. They can not be interacted with.
As result, such objects can be treated in the separate way:
1. It is possible to tweak groundcover objects density.
2. It is possible to safely merge such objects even near player.
3. Such objects can be animated (to simulate wind, for example).
4. Some parts of processing can be skipped.
For example, we do not need to have collision or animation objects for groundcover,
do not need to render groundcover on the map, do not need to render it for the whole visible area (which can be very large with Distant Terrain). It allows to increase performance a lot.
General advices to create assets for this feature:
1. Alpha properties from Nif files are not used, a unified alpha settings are used (alpha testing, "greater of equal" function, 128/255 threshold).
2. Use a single NiTriShape in groundocver mesh, or at least use same properties (texture, alpha, material, etc), so OpenMW can merge them on the fly. Otherwise animations may not work properly.
3. Smooth fading does not work for meshes, which have textures without alpha (e.g. rock).
Groundcover mods can be registered in the openmw.cfg via "groundcover" entries instead of "content" ones:
::
groundcover=my_grass_mod.esp
Every static from such mod is treated as a groundcover object.
Also groundcover detection should be enabled via settings.cfg:
::
[Groundcover]
enabled = true
.. _`Graphic Herbalism`: https://www.nexusmods.com/morrowind/mods/46599
.. _`OpenMW Containers Animated`: https://www.nexusmods.com/morrowind/mods/46232
.. _`Glow in the Dahrk`: https://www.nexusmods.com/morrowind/mods/45886

@ -0,0 +1,68 @@
Groundcover Settings
####################
enabled
-------
:Type: boolean
:Range: True/False
:Default: False
Allows the engine to use groundcover.
Groundcover objects are static objects which come from ESP files, registered via
"groundcover" entries from openmw.cfg rather than "content" ones.
We assume that groundcover objects have no collisions, can not be moved or interacted with,
so we can merge them to pages and animate them indifferently from distance from player.
This setting can only be configured by editing the settings configuration file.
fade start
----------
:Type: floating point
:Range: 0.0 to 1.0
:Default: 0.85
Determines on which distance from player groundcover fading starts.
Does not work for meshes which textures do not have transparency (e.g. rocks).
This setting can only be configured by editing the settings configuration file.
density
-------
:Type: floating point
:Range: 0.0 (0%) to 1.0 (100%)
:Default: 1.0
Determines how many groundcover instances from content files
are used in the game. Can affect performance a lot.
This setting can only be configured by editing the settings configuration file.
distance
--------
:Type: integer
:Range: > 0
:Default: 1
Determines on which distance in cells grass pages are rendered.
Default 1 value means 3x3 cells area (active grid).
May affect performance a lot.
This setting can only be configured by editing the settings configuration file.
min chunk size
--------------
:Type: floating point
:Range: 0.125, 0.25, 0.5, 1.0
:Default: 0.5
Determines a minimum size of groundcover chunks in cells. For example, with 0.5 value
chunks near player will have size 4096x4096 game units. Larger chunks reduce CPU usage
(Draw and Cull bars), but can increase GPU usage (GPU bar) since culling becomes less efficient.
Smaller values do an opposite.
This setting can only be configured by editing the settings configuration file.

@ -42,6 +42,7 @@ The ranges included with each setting are the physically possible ranges, not re
camera
cells
fog
groundcover
map
GUI
HUD

@ -26,6 +26,7 @@ Has no effect if the 'force shaders' option is false.
Enabling per-pixel lighting results in visual differences to the original MW engine.
It is not recommended to enable this option when using vanilla Morrowind files,
because certain lights in Morrowind rely on vertex lighting to look as intended.
Note that groundcover shaders ignore this setting.
clamp lighting
--------------

@ -62,7 +62,7 @@ reflection detail
-----------------
:Type: integer
:Range: 0, 1, 2, 3, 4
:Range: 0, 1, 2, 3, 4, 5
:Default: 2
Controls what kinds of things are rendered in water reflections.
@ -72,6 +72,7 @@ Controls what kinds of things are rendered in water reflections.
2: statics, activators, and doors are also reflected
3: items, containers, and particles are also reflected
4: actors are also reflected
5: groundcover objects are also reflected
In interiors the lowest level is 2.
This setting can be changed ingame with the "Reflection shader detail" dropdown under the Water tab of the Video panel in the Options menu.

@ -449,6 +449,7 @@
<Property key="AddItem" value="World"/>
<Property key="AddItem" value="Objects"/>
<Property key="AddItem" value="Actors"/>
<Property key="AddItem" value="Groundcover"/>
</Widget>
<Widget type="AutoSizedTextBox" skin="SandText" position="64 4 90 16" align="Left Top">
<Property key="Caption" value="Reflection shader detail"/>

@ -950,6 +950,25 @@ lineofsight keep inactive cache = 0
defer aabb update = true
[Models]
# Attempt to load any valid NIF file regardless of its version and track the progress.
# Loading arbitrary meshes is not advised and may cause instability.
load unsupported nif files = false
[Groundcover]
# enable separate groundcover handling
enabled = false
# configure groundcover fade out threshold
fade start = 0.85
# A groundcover density (0.0 <= value <= 1.0)
# 1.0 means 100% density
density = 1.0
# A maximum distance in cells on which groundcover is rendered.
distance = 1
# A minimum size of groundcover chunk in cells (0.125, 0.25, 0.5, 1.0)
min chunk size = 0.5

@ -7,6 +7,8 @@ set(SDIR ${CMAKE_CURRENT_SOURCE_DIR})
set(DDIRRELATIVE resources/shaders)
set(SHADER_FILES
groundcover_vertex.glsl
groundcover_fragment.glsl
water_vertex.glsl
water_fragment.glsl
water_nm.png

@ -0,0 +1,93 @@
#version 120
#define GROUNDCOVER
#if @diffuseMap
uniform sampler2D diffuseMap;
varying vec2 diffuseMapUV;
#endif
#if @normalMap
uniform sampler2D normalMap;
varying vec2 normalMapUV;
varying vec4 passTangent;
#endif
// Other shaders respect forcePPL, but legacy groundcover mods were designed to work with vertex lighting.
// They may do not look as intended with per-pixel lighting, so ignore this setting for now.
#define PER_PIXEL_LIGHTING @normalMap
varying float euclideanDepth;
varying float linearDepth;
#if PER_PIXEL_LIGHTING
varying vec3 passViewPos;
varying vec3 passNormal;
#else
centroid varying vec3 passLighting;
centroid varying vec3 shadowDiffuseLighting;
#endif
#include "shadows_fragment.glsl"
#include "lighting.glsl"
float calc_coverage(float a, float alpha_ref, float falloff_rate)
{
return clamp(falloff_rate * (a - alpha_ref) + alpha_ref, 0.0, 1.0);
}
void main()
{
#if @normalMap
vec4 normalTex = texture2D(normalMap, normalMapUV);
vec3 normalizedNormal = normalize(passNormal);
vec3 normalizedTangent = normalize(passTangent.xyz);
vec3 binormal = cross(normalizedTangent, normalizedNormal) * passTangent.w;
mat3 tbnTranspose = mat3(normalizedTangent, binormal, normalizedNormal);
vec3 viewNormal = gl_NormalMatrix * normalize(tbnTranspose * (normalTex.xyz * 2.0 - 1.0));
#endif
#if (!@normalMap && @forcePPL && false)
vec3 viewNormal = gl_NormalMatrix * normalize(passNormal);
#endif
#if @diffuseMap
gl_FragData[0] = texture2D(diffuseMap, diffuseMapUV);
#else
gl_FragData[0] = vec4(1.0);
#endif
gl_FragData[0].a = calc_coverage(gl_FragData[0].a, 128.0/255.0, 4.0);
float shadowing = unshadowedLightRatio(linearDepth);
if (euclideanDepth > @groundcoverFadeStart)
gl_FragData[0].a *= 1.0-smoothstep(@groundcoverFadeStart, @groundcoverFadeEnd, euclideanDepth);
vec3 lighting;
#if !PER_PIXEL_LIGHTING
lighting = passLighting + shadowDiffuseLighting * shadowing;
#else
vec3 diffuseLight, ambientLight;
doLighting(passViewPos, normalize(viewNormal), shadowing, diffuseLight, ambientLight);
lighting = diffuseLight + ambientLight;
#endif
#if @clamp
lighting = clamp(lighting, vec3(0.0), vec3(1.0));
#else
lighting = max(lighting, 0.0);
#endif
gl_FragData[0].xyz *= lighting;
#if @radialFog
float fogValue = clamp((euclideanDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0);
#else
float fogValue = clamp((linearDepth - gl_Fog.start) * gl_Fog.scale, 0.0, 1.0);
#endif
gl_FragData[0].xyz = mix(gl_FragData[0].xyz, gl_Fog.color.xyz, fogValue);
applyShadowDebugOverlay();
}

@ -0,0 +1,143 @@
#version 120
#define GROUNDCOVER
attribute vec4 aOffset;
attribute vec3 aRotation;
#if @diffuseMap
varying vec2 diffuseMapUV;
#endif
#if @normalMap
varying vec2 normalMapUV;
varying vec4 passTangent;
#endif
// Other shaders respect forcePPL, but legacy groundcover mods were designed to work with vertex lighting.
// They may do not look as intended with per-pixel lighting, so ignore this setting for now.
#define PER_PIXEL_LIGHTING @normalMap
varying float euclideanDepth;
varying float linearDepth;
#if PER_PIXEL_LIGHTING
varying vec3 passViewPos;
varying vec3 passNormal;
#else
centroid varying vec3 passLighting;
centroid varying vec3 shadowDiffuseLighting;
#endif
#include "shadows_vertex.glsl"
#include "lighting.glsl"
uniform float osg_SimulationTime;
uniform mat4 osg_ViewMatrixInverse;
uniform mat4 osg_ViewMatrix;
uniform float windSpeed;
uniform vec3 playerPos;
vec2 groundcoverDisplacement(in vec3 worldpos, float h)
{
vec2 windDirection = vec2(1.0);
vec3 footPos = playerPos;
vec3 windVec = vec3(windSpeed * windDirection, 1.0);
float v = length(windVec);
vec2 displace = vec2(2.0 * windVec + 0.1);
vec2 harmonics = vec2(0.0);
harmonics += vec2((1.0 - 0.10*v) * sin(1.0*osg_SimulationTime + worldpos.xy / 1100.0));
harmonics += vec2((1.0 - 0.04*v) * cos(2.0*osg_SimulationTime + worldpos.xy / 750.0));
harmonics += vec2((1.0 + 0.14*v) * sin(3.0*osg_SimulationTime + worldpos.xy / 500.0));
harmonics += vec2((1.0 + 0.28*v) * sin(5.0*osg_SimulationTime + worldpos.xy / 200.0));
// FIXME: stomping function does not work well in MGE:
// 1. It does not take in account Z coordinate, so it works even when player levitates.
// 2. It works more-or-less well only for grass meshes, but not for other types of plants.
// So disable this function for now, until we find a better one.
vec2 stomp = vec2(0.0);
//float d = length(worldpos.xy - footPos.xy);
//if (d < 150.0 && d > 0.0)
//{
// stomp = (60.0 / d - 0.4) * (worldpos.xy - footPos.xy);
//}
return clamp(0.02 * h, 0.0, 1.0) * (harmonics * displace + stomp);
}
mat4 rotation(in vec3 angle)
{
float sin_x = sin(angle.x);
float cos_x = cos(angle.x);
float sin_y = sin(angle.y);
float cos_y = cos(angle.y);
float sin_z = sin(angle.z);
float cos_z = cos(angle.z);
return mat4(
cos_z*cos_y+sin_x*sin_y*sin_z, -sin_z*cos_x, cos_z*sin_y+sin_z*sin_x*cos_y, 0.0,
sin_z*cos_y+cos_z*sin_x*sin_y, cos_z*cos_x, sin_z*sin_y-cos_z*sin_x*cos_y, 0.0,
-sin_y*cos_x, sin_x, cos_x*cos_y, 0.0,
0.0, 0.0, 0.0, 1.0);
}
mat3 rotation3(in mat4 rot4)
{
return mat3(
rot4[0].xyz,
rot4[1].xyz,
rot4[2].xyz);
}
void main(void)
{
vec3 position = aOffset.xyz;
float scale = aOffset.w;
mat4 rotation = rotation(aRotation);
vec4 displacedVertex = rotation * scale * gl_Vertex;
displacedVertex = vec4(displacedVertex.xyz + position, 1.0);
vec4 worldPos = osg_ViewMatrixInverse * gl_ModelViewMatrix * displacedVertex;
worldPos.xy += groundcoverDisplacement(worldPos.xyz, gl_Vertex.z);
vec4 viewPos = osg_ViewMatrix * worldPos;
gl_ClipVertex = viewPos;
euclideanDepth = length(viewPos.xyz);
if (length(gl_ModelViewMatrix * vec4(position, 1.0)) > @groundcoverFadeEnd)
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
else
gl_Position = gl_ProjectionMatrix * viewPos;
linearDepth = gl_Position.z;
#if (!PER_PIXEL_LIGHTING || @shadows_enabled)
vec3 viewNormal = normalize((gl_NormalMatrix * rotation3(rotation) * gl_Normal).xyz);
#endif
#if @diffuseMap
diffuseMapUV = (gl_TextureMatrix[@diffuseMapUV] * gl_MultiTexCoord@diffuseMapUV).xy;
#endif
#if @normalMap
normalMapUV = (gl_TextureMatrix[@normalMapUV] * gl_MultiTexCoord@normalMapUV).xy;
passTangent = gl_MultiTexCoord7.xyzw * rotation;
#endif
#if PER_PIXEL_LIGHTING
passViewPos = viewPos.xyz;
passNormal = rotation3(rotation) * gl_Normal.xyz;
#else
vec3 diffuseLight, ambientLight;
doLighting(viewPos.xyz, viewNormal, diffuseLight, ambientLight, shadowDiffuseLighting);
passLighting = diffuseLight + ambientLight;
#endif
#if (@shadows_enabled)
setupShadowCoords(viewPos, viewNormal);
#endif
}

@ -8,7 +8,20 @@ void perLight(out vec3 ambientOut, out vec3 diffuseOut, int lightIndex, vec3 vie
float illumination = clamp(1.0 / (gl_LightSource[lightIndex].constantAttenuation + gl_LightSource[lightIndex].linearAttenuation * lightDistance + gl_LightSource[lightIndex].quadraticAttenuation * lightDistance * lightDistance), 0.0, 1.0);
ambientOut = gl_LightSource[lightIndex].ambient.xyz * illumination;
diffuseOut = gl_LightSource[lightIndex].diffuse.xyz * max(dot(viewNormal, lightDir), 0.0) * illumination;
float lambert = dot(viewNormal.xyz, lightDir) * illumination;
#ifndef GROUNDCOVER
lambert = max(lambert, 0.0);
#else
{
// might need to be < 0 depending on direction of viewPos
if (dot(viewPos, viewNormal.xyz) > 0)
lambert = -lambert;
if (lambert < 0)
lambert *= -0.3;
}
#endif
diffuseOut = gl_LightSource[lightIndex].diffuse.xyz * lambert;
}
#if PER_PIXEL_LIGHTING

Loading…
Cancel
Save