mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-21 09:23:53 +00:00
Merge pull request #2225 from elsid/retry_async_navmesh_update_job
Support explicit limit of navmesh tiles for scene
This commit is contained in:
commit
c2a7aa2932
12 changed files with 212 additions and 94 deletions
|
@ -61,6 +61,7 @@ namespace
|
|||
mSettings.mMaxSmoothPathSize = 1024;
|
||||
mSettings.mTrianglesPerChunk = 256;
|
||||
mSettings.mMaxPolys = 4096;
|
||||
mSettings.mMaxTilesNumber = 512;
|
||||
mNavigator.reset(new NavigatorImpl(mSettings));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#include "asyncnavmeshupdater.hpp"
|
||||
#include "asyncnavmeshupdater.hpp"
|
||||
#include "debug.hpp"
|
||||
#include "makenavmesh.hpp"
|
||||
#include "settings.hpp"
|
||||
|
@ -14,16 +14,6 @@ namespace
|
|||
{
|
||||
return std::abs(lhs.x() - rhs.x()) + std::abs(lhs.y() - rhs.y());
|
||||
}
|
||||
|
||||
std::tuple<ChangeType, int, int> makePriority(const TilePosition& position, const ChangeType changeType,
|
||||
const TilePosition& playerTile)
|
||||
{
|
||||
return std::make_tuple(
|
||||
changeType,
|
||||
getManhattanDistance(position, playerTile),
|
||||
getManhattanDistance(position, TilePosition {0, 0})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
namespace DetourNavigator
|
||||
|
@ -32,14 +22,18 @@ namespace DetourNavigator
|
|||
{
|
||||
switch (value)
|
||||
{
|
||||
case UpdateNavMeshStatus::ignore:
|
||||
case UpdateNavMeshStatus::ignored:
|
||||
return stream << "ignore";
|
||||
case UpdateNavMeshStatus::removed:
|
||||
return stream << "removed";
|
||||
case UpdateNavMeshStatus::add:
|
||||
case UpdateNavMeshStatus::added:
|
||||
return stream << "add";
|
||||
case UpdateNavMeshStatus::replaced:
|
||||
return stream << "replaced";
|
||||
case UpdateNavMeshStatus::failed:
|
||||
return stream << "failed";
|
||||
case UpdateNavMeshStatus::lost:
|
||||
return stream << "lost";
|
||||
}
|
||||
return stream << "unknown";
|
||||
}
|
||||
|
@ -81,13 +75,25 @@ namespace DetourNavigator
|
|||
for (const auto& changedTile : changedTiles)
|
||||
{
|
||||
if (mPushed[agentHalfExtents].insert(changedTile.first).second)
|
||||
mJobs.push(Job {agentHalfExtents, navMeshCacheItem, changedTile.first,
|
||||
makePriority(changedTile.first, changedTile.second, playerTile)});
|
||||
{
|
||||
Job job;
|
||||
|
||||
job.mAgentHalfExtents = agentHalfExtents;
|
||||
job.mNavMeshCacheItem = navMeshCacheItem;
|
||||
job.mChangedTile = changedTile.first;
|
||||
job.mTryNumber = 0;
|
||||
job.mChangeType = changedTile.second;
|
||||
job.mDistanceToPlayer = getManhattanDistance(changedTile.first, playerTile);
|
||||
job.mDistanceToOrigin = getManhattanDistance(changedTile.first, TilePosition {0, 0});
|
||||
|
||||
mJobs.push(std::move(job));
|
||||
}
|
||||
}
|
||||
|
||||
log("posted ", mJobs.size(), " jobs");
|
||||
|
||||
mHasJob.notify_all();
|
||||
if (!mJobs.empty())
|
||||
mHasJob.notify_all();
|
||||
}
|
||||
|
||||
void AsyncNavMeshUpdater::wait()
|
||||
|
@ -103,8 +109,9 @@ namespace DetourNavigator
|
|||
{
|
||||
try
|
||||
{
|
||||
if (const auto job = getNextJob())
|
||||
processJob(*job);
|
||||
if (auto job = getNextJob())
|
||||
if (!processJob(*job))
|
||||
repost(std::move(*job));
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
|
@ -114,7 +121,7 @@ namespace DetourNavigator
|
|||
log("stop process jobs");
|
||||
}
|
||||
|
||||
void AsyncNavMeshUpdater::processJob(const Job& job)
|
||||
bool AsyncNavMeshUpdater::processJob(const Job& job)
|
||||
{
|
||||
log("process job for agent=", job.mAgentHalfExtents);
|
||||
|
||||
|
@ -135,12 +142,16 @@ namespace DetourNavigator
|
|||
|
||||
using FloatMs = std::chrono::duration<float, std::milli>;
|
||||
|
||||
const auto locked = job.mNavMeshCacheItem.lockConst();
|
||||
log("cache updated for agent=", job.mAgentHalfExtents, " status=", status,
|
||||
" generation=", locked->getGeneration(),
|
||||
" revision=", locked->getNavMeshRevision(),
|
||||
" time=", std::chrono::duration_cast<FloatMs>(finish - start).count(), "ms",
|
||||
" total_time=", std::chrono::duration_cast<FloatMs>(finish - firstStart).count(), "ms");
|
||||
{
|
||||
const auto locked = job.mNavMeshCacheItem.lockConst();
|
||||
log("cache updated for agent=", job.mAgentHalfExtents, " status=", status,
|
||||
" generation=", locked->getGeneration(),
|
||||
" revision=", locked->getNavMeshRevision(),
|
||||
" time=", std::chrono::duration_cast<FloatMs>(finish - start).count(), "ms",
|
||||
" total_time=", std::chrono::duration_cast<FloatMs>(finish - firstStart).count(), "ms");
|
||||
}
|
||||
|
||||
return isSuccess(status);
|
||||
}
|
||||
|
||||
boost::optional<AsyncNavMeshUpdater::Job> AsyncNavMeshUpdater::getNextJob()
|
||||
|
@ -193,4 +204,19 @@ namespace DetourNavigator
|
|||
*locked = value;
|
||||
return *locked.get();
|
||||
}
|
||||
|
||||
void AsyncNavMeshUpdater::repost(Job&& job)
|
||||
{
|
||||
if (mShouldStop || job.mTryNumber > 2)
|
||||
return;
|
||||
|
||||
const std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
if (mPushed[job.mAgentHalfExtents].insert(job.mChangedTile).second)
|
||||
{
|
||||
++job.mTryNumber;
|
||||
mJobs.push(std::move(job));
|
||||
mHasJob.notify_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,11 +50,19 @@ namespace DetourNavigator
|
|||
osg::Vec3f mAgentHalfExtents;
|
||||
SharedNavMeshCacheItem mNavMeshCacheItem;
|
||||
TilePosition mChangedTile;
|
||||
std::tuple<ChangeType, int, int> mPriority;
|
||||
unsigned mTryNumber;
|
||||
ChangeType mChangeType;
|
||||
int mDistanceToPlayer;
|
||||
int mDistanceToOrigin;
|
||||
|
||||
std::tuple<unsigned, ChangeType, int, int> getPriority() const
|
||||
{
|
||||
return std::make_tuple(mTryNumber, mChangeType, mDistanceToPlayer, mDistanceToOrigin);
|
||||
}
|
||||
|
||||
friend inline bool operator <(const Job& lhs, const Job& rhs)
|
||||
{
|
||||
return lhs.mPriority > rhs.mPriority;
|
||||
return lhs.getPriority() > rhs.getPriority();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -76,13 +84,15 @@ namespace DetourNavigator
|
|||
|
||||
void process() throw();
|
||||
|
||||
void processJob(const Job& job);
|
||||
bool processJob(const Job& job);
|
||||
|
||||
boost::optional<Job> getNextJob();
|
||||
|
||||
void writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const;
|
||||
|
||||
std::chrono::steady_clock::time_point setFirstStart(const std::chrono::steady_clock::time_point& value);
|
||||
|
||||
void repost(Job&& job);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -441,17 +441,56 @@ namespace
|
|||
return NavMeshData(navMeshData, navMeshDataSize);
|
||||
}
|
||||
|
||||
UpdateNavMeshStatus makeUpdateNavMeshStatus(bool removed, bool add)
|
||||
class UpdateNavMeshStatusBuilder
|
||||
{
|
||||
if (removed && add)
|
||||
return UpdateNavMeshStatus::replaced;
|
||||
else if (removed)
|
||||
return UpdateNavMeshStatus::removed;
|
||||
else if (add)
|
||||
return UpdateNavMeshStatus::add;
|
||||
else
|
||||
return UpdateNavMeshStatus::ignore;
|
||||
}
|
||||
public:
|
||||
UpdateNavMeshStatusBuilder() = default;
|
||||
|
||||
UpdateNavMeshStatusBuilder removed(bool value)
|
||||
{
|
||||
if (value)
|
||||
set(UpdateNavMeshStatus::removed);
|
||||
else
|
||||
unset(UpdateNavMeshStatus::removed);
|
||||
return *this;
|
||||
}
|
||||
|
||||
UpdateNavMeshStatusBuilder added(bool value)
|
||||
{
|
||||
if (value)
|
||||
set(UpdateNavMeshStatus::added);
|
||||
else
|
||||
unset(UpdateNavMeshStatus::added);
|
||||
return *this;
|
||||
}
|
||||
|
||||
UpdateNavMeshStatusBuilder failed(bool value)
|
||||
{
|
||||
if (value)
|
||||
set(UpdateNavMeshStatus::failed);
|
||||
else
|
||||
unset(UpdateNavMeshStatus::failed);
|
||||
return *this;
|
||||
}
|
||||
|
||||
UpdateNavMeshStatus getResult() const
|
||||
{
|
||||
return mResult;
|
||||
}
|
||||
|
||||
private:
|
||||
UpdateNavMeshStatus mResult = UpdateNavMeshStatus::ignored;
|
||||
|
||||
void set(UpdateNavMeshStatus value)
|
||||
{
|
||||
mResult = static_cast<UpdateNavMeshStatus>(static_cast<unsigned>(mResult) | static_cast<unsigned>(value));
|
||||
}
|
||||
|
||||
void unset(UpdateNavMeshStatus value)
|
||||
{
|
||||
mResult = static_cast<UpdateNavMeshStatus>(static_cast<unsigned>(mResult) & ~static_cast<unsigned>(value));
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
unsigned long getMinValuableBitsNumber(const T value)
|
||||
|
@ -461,6 +500,49 @@ namespace
|
|||
++power;
|
||||
return power;
|
||||
}
|
||||
|
||||
dtStatus addTile(dtNavMesh& navMesh, const NavMeshData& navMeshData)
|
||||
{
|
||||
const dtTileRef lastRef = 0;
|
||||
dtTileRef* const result = nullptr;
|
||||
return navMesh.addTile(navMeshData.mValue.get(), navMeshData.mSize,
|
||||
doNotTransferOwnership, lastRef, result);
|
||||
}
|
||||
|
||||
dtStatus addTile(dtNavMesh& navMesh, const NavMeshTilesCache::Value& cachedNavMeshData)
|
||||
{
|
||||
const dtTileRef lastRef = 0;
|
||||
dtTileRef* const result = nullptr;
|
||||
return navMesh.addTile(cachedNavMeshData.get().mValue, cachedNavMeshData.get().mSize,
|
||||
doNotTransferOwnership, lastRef, result);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
UpdateNavMeshStatus replaceTile(const SharedNavMeshCacheItem& navMeshCacheItem,
|
||||
const TilePosition& changedTile, T&& navMeshData)
|
||||
{
|
||||
const auto locked = navMeshCacheItem.lock();
|
||||
auto& navMesh = locked->getValue();
|
||||
const int layer = 0;
|
||||
const auto tileRef = navMesh.getTileRefAt(changedTile.x(), changedTile.y(), layer);
|
||||
unsigned char** const data = nullptr;
|
||||
int* const dataSize = nullptr;
|
||||
const auto removed = dtStatusSucceed(navMesh.removeTile(tileRef, data, dataSize));
|
||||
const auto addStatus = addTile(navMesh, navMeshData);
|
||||
|
||||
if (dtStatusSucceed(addStatus))
|
||||
{
|
||||
locked->setUsedTile(changedTile, std::forward<T>(navMeshData));
|
||||
return UpdateNavMeshStatusBuilder().added(true).removed(removed).getResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (removed)
|
||||
locked->removeUsedTile(changedTile);
|
||||
log("failed to add tile with status=", WriteDtStatus {addStatus});
|
||||
return UpdateNavMeshStatusBuilder().removed(removed).failed((addStatus & DT_OUT_OF_MEMORY) != 0).getResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace DetourNavigator
|
||||
|
@ -522,7 +604,7 @@ namespace DetourNavigator
|
|||
const auto removed = dtStatusSucceed(navMesh.removeTile(tileRef, nullptr, nullptr));
|
||||
if (removed)
|
||||
locked->removeUsedTile(changedTile);
|
||||
return makeUpdateNavMeshStatus(removed, false);
|
||||
return UpdateNavMeshStatusBuilder().removed(removed).getResult();
|
||||
};
|
||||
|
||||
if (!recastMesh)
|
||||
|
@ -546,7 +628,7 @@ namespace DetourNavigator
|
|||
return removeTile();
|
||||
}
|
||||
|
||||
if (!shouldAddTile(changedTile, playerTile, params.maxTiles))
|
||||
if (!shouldAddTile(changedTile, playerTile, std::min(settings.mMaxTilesNumber, params.maxTiles)))
|
||||
{
|
||||
log("ignore add tile: too far from player");
|
||||
return removeTile();
|
||||
|
@ -583,47 +665,10 @@ namespace DetourNavigator
|
|||
if (!cachedNavMeshData)
|
||||
{
|
||||
log("cache overflow");
|
||||
|
||||
const auto locked = navMeshCacheItem.lock();
|
||||
auto& navMesh = locked->getValue();
|
||||
const auto tileRef = navMesh.getTileRefAt(x, y, 0);
|
||||
const auto removed = dtStatusSucceed(navMesh.removeTile(tileRef, nullptr, nullptr));
|
||||
const auto addStatus = navMesh.addTile(navMeshData.mValue.get(), navMeshData.mSize,
|
||||
doNotTransferOwnership, 0, 0);
|
||||
|
||||
if (dtStatusSucceed(addStatus))
|
||||
{
|
||||
locked->setUsedTile(changedTile, std::move(navMeshData));
|
||||
return makeUpdateNavMeshStatus(removed, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (removed)
|
||||
locked->removeUsedTile(changedTile);
|
||||
log("failed to add tile with status=", WriteDtStatus {addStatus});
|
||||
return makeUpdateNavMeshStatus(removed, false);
|
||||
}
|
||||
return replaceTile(navMeshCacheItem, changedTile, std::move(navMeshData));
|
||||
}
|
||||
}
|
||||
|
||||
const auto locked = navMeshCacheItem.lock();
|
||||
auto& navMesh = locked->getValue();
|
||||
const auto tileRef = navMesh.getTileRefAt(x, y, 0);
|
||||
const auto removed = dtStatusSucceed(navMesh.removeTile(tileRef, nullptr, nullptr));
|
||||
const auto addStatus = navMesh.addTile(cachedNavMeshData.get().mValue, cachedNavMeshData.get().mSize,
|
||||
doNotTransferOwnership, 0, 0);
|
||||
|
||||
if (dtStatusSucceed(addStatus))
|
||||
{
|
||||
locked->setUsedTile(changedTile, std::move(cachedNavMeshData));
|
||||
return makeUpdateNavMeshStatus(removed, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (removed)
|
||||
locked->removeUsedTile(changedTile);
|
||||
log("failed to add tile with status=", WriteDtStatus {addStatus});
|
||||
return makeUpdateNavMeshStatus(removed, false);
|
||||
}
|
||||
return replaceTile(navMeshCacheItem, changedTile, std::move(cachedNavMeshData));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,14 +20,21 @@ namespace DetourNavigator
|
|||
class RecastMesh;
|
||||
struct Settings;
|
||||
|
||||
enum class UpdateNavMeshStatus
|
||||
enum class UpdateNavMeshStatus : unsigned
|
||||
{
|
||||
ignore,
|
||||
removed,
|
||||
add,
|
||||
replaced
|
||||
ignored = 0,
|
||||
removed = 1 << 0,
|
||||
added = 1 << 1,
|
||||
replaced = removed | added,
|
||||
failed = 1 << 2,
|
||||
lost = removed | failed,
|
||||
};
|
||||
|
||||
inline bool isSuccess(UpdateNavMeshStatus value)
|
||||
{
|
||||
return (static_cast<unsigned>(value) & static_cast<unsigned>(UpdateNavMeshStatus::failed)) == 0;
|
||||
}
|
||||
|
||||
inline float getLength(const osg::Vec2i& value)
|
||||
{
|
||||
return std::sqrt(float(osg::square(value.x()) + osg::square(value.y())));
|
||||
|
@ -41,7 +48,7 @@ namespace DetourNavigator
|
|||
inline bool shouldAddTile(const TilePosition& changedTile, const TilePosition& playerTile, int maxTiles)
|
||||
{
|
||||
const auto expectedTilesCount = std::ceil(osg::PI * osg::square(getDistance(changedTile, playerTile)));
|
||||
return expectedTilesCount * 3 <= maxTiles;
|
||||
return expectedTilesCount <= maxTiles;
|
||||
}
|
||||
|
||||
NavMeshPtr makeEmptyNavMesh(const Settings& settings);
|
||||
|
|
|
@ -157,7 +157,7 @@ namespace DetourNavigator
|
|||
if (changedTiles->second.empty())
|
||||
mChangedTiles.erase(changedTiles);
|
||||
}
|
||||
const auto maxTiles = navMesh.getParams()->maxTiles;
|
||||
const auto maxTiles = std::min(mSettings.mMaxTilesNumber, navMesh.getParams()->maxTiles);
|
||||
mRecastMeshManager.forEachTilePosition([&] (const TilePosition& tile)
|
||||
{
|
||||
if (tilesToPost.count(tile))
|
||||
|
|
|
@ -62,8 +62,8 @@ namespace DetourNavigator
|
|||
return Value();
|
||||
|
||||
// TODO: use different function to make key to avoid unnecessary std::string allocation
|
||||
const auto tile = tileValues->second.Map.find(makeNavMeshKey(recastMesh, offMeshConnections));
|
||||
if (tile == tileValues->second.Map.end())
|
||||
const auto tile = tileValues->second.mMap.find(makeNavMeshKey(recastMesh, offMeshConnections));
|
||||
if (tile == tileValues->second.mMap.end())
|
||||
return Value();
|
||||
|
||||
acquireItemUnsafe(tile->second);
|
||||
|
@ -96,7 +96,7 @@ namespace DetourNavigator
|
|||
|
||||
const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentHalfExtents, changedTile, navMeshKey);
|
||||
// TODO: use std::string_view or some alternative to avoid navMeshKey copy into both mFreeItems and mValues
|
||||
const auto emplaced = mValues[agentHalfExtents][changedTile].Map.emplace(navMeshKey, iterator);
|
||||
const auto emplaced = mValues[agentHalfExtents][changedTile].mMap.emplace(navMeshKey, iterator);
|
||||
|
||||
if (!emplaced.second)
|
||||
{
|
||||
|
@ -125,16 +125,16 @@ namespace DetourNavigator
|
|||
if (tileValues == agentValues->second.end())
|
||||
return;
|
||||
|
||||
const auto value = tileValues->second.Map.find(item.mNavMeshKey);
|
||||
if (value == tileValues->second.Map.end())
|
||||
const auto value = tileValues->second.mMap.find(item.mNavMeshKey);
|
||||
if (value == tileValues->second.mMap.end())
|
||||
return;
|
||||
|
||||
mUsedNavMeshDataSize -= getSize(item);
|
||||
mFreeNavMeshDataSize -= getSize(item);
|
||||
mFreeItems.pop_back();
|
||||
|
||||
tileValues->second.Map.erase(value);
|
||||
if (!tileValues->second.Map.empty())
|
||||
tileValues->second.mMap.erase(value);
|
||||
if (!tileValues->second.mMap.empty())
|
||||
return;
|
||||
|
||||
agentValues->second.erase(tileValues);
|
||||
|
|
|
@ -108,7 +108,7 @@ namespace DetourNavigator
|
|||
|
||||
struct TileMap
|
||||
{
|
||||
std::map<std::string, ItemIterator> Map;
|
||||
std::map<std::string, ItemIterator> mMap;
|
||||
};
|
||||
|
||||
std::mutex mMutex;
|
||||
|
|
|
@ -24,6 +24,7 @@ namespace DetourNavigator
|
|||
navigatorSettings.mMaxEdgeLen = ::Settings::Manager::getInt("max edge len", "Navigator");
|
||||
navigatorSettings.mMaxNavMeshQueryNodes = ::Settings::Manager::getInt("max nav mesh query nodes", "Navigator");
|
||||
navigatorSettings.mMaxPolys = ::Settings::Manager::getInt("max polygons per tile", "Navigator");
|
||||
navigatorSettings.mMaxTilesNumber = ::Settings::Manager::getInt("max tiles number", "Navigator");
|
||||
navigatorSettings.mMaxVertsPerPoly = ::Settings::Manager::getInt("max verts per poly", "Navigator");
|
||||
navigatorSettings.mRegionMergeSize = ::Settings::Manager::getInt("region merge size", "Navigator");
|
||||
navigatorSettings.mRegionMinSize = ::Settings::Manager::getInt("region min size", "Navigator");
|
||||
|
|
|
@ -26,6 +26,7 @@ namespace DetourNavigator
|
|||
int mMaxEdgeLen = 0;
|
||||
int mMaxNavMeshQueryNodes = 0;
|
||||
int mMaxPolys = 0;
|
||||
int mMaxTilesNumber = 0;
|
||||
int mMaxVertsPerPoly = 0;
|
||||
int mRegionMergeSize = 0;
|
||||
int mRegionMinSize = 0;
|
||||
|
|
|
@ -24,6 +24,24 @@ Moving across external world, entering/exiting location produce nav mesh update.
|
|||
NPC and creatures may not be able to find path before nav mesh is built around them.
|
||||
Try to disable this if you want to have old fashioned AI which doesn't know where to go when you stand behind that stone and casting a firebolt.
|
||||
|
||||
max tiles number
|
||||
----------------
|
||||
|
||||
:Type: integer
|
||||
:Range: >= 0
|
||||
:Default: 512
|
||||
|
||||
Number of tiles at nav mesh.
|
||||
Nav mesh covers circle area around player.
|
||||
This option allows to set an explicit limit for nav mesh size, how many tiles should fit into circle.
|
||||
If actor is inside this area it able to find path over nav mesh.
|
||||
Increasing this value may decrease performance.
|
||||
|
||||
.. note::
|
||||
Don't expect infinite nav mesh size increasing.
|
||||
This condition is always true: ``max tiles number * max polygons per tile <= 4194304``.
|
||||
It's a limitation of `Recastnavigation <https://github.com/recastnavigation/recastnavigation>`_ library.
|
||||
|
||||
Advanced settings
|
||||
*****************
|
||||
|
||||
|
@ -322,6 +340,12 @@ Maximum number of polygons per nav mesh tile. Maximum number of nav mesh tiles d
|
|||
this value. 22 bits is a limit to store both tile identifier and polygon identifier (tiles = 2^(22 - log2(polygons))).
|
||||
See `recastnavigation <https://github.com/recastnavigation/recastnavigation>`_ for more details.
|
||||
|
||||
.. Warning::
|
||||
Lower value may lead to ignored world geometry on nav mesh.
|
||||
Greater value will reduce number of nav mesh tiles.
|
||||
This condition is always true: ``max tiles number * max polygons per tile <= 4194304``.
|
||||
It's a limitation of `Recastnavigation <https://github.com/recastnavigation/recastnavigation>`_ library.
|
||||
|
||||
max verts per poly
|
||||
------------------
|
||||
|
||||
|
|
|
@ -670,6 +670,9 @@ enable nav mesh render = false
|
|||
# Render agents paths (true, false)
|
||||
enable agents paths render = false
|
||||
|
||||
# Max number of navmesh tiles (value >= 0)
|
||||
max tiles number = 512
|
||||
|
||||
[Shadows]
|
||||
|
||||
# Enable or disable shadows. Bear in mind that this will force OpenMW to use shaders as if "[Shaders]/force shaders" was set to true.
|
||||
|
|
Loading…
Reference in a new issue