Merge pull request #1633 from elsid/pathfinder_detour

Use recastnavigation for pathfinding (#2229)
pull/541/head
Bret Curtis 6 years ago committed by GitHub
commit d6c674660a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,3 @@
#!/bin/sh
#!/bin/sh -e
sudo ln -s /usr/bin/clang-3.6 /usr/local/bin/clang
sudo ln -s /usr/bin/clang++-3.6 /usr/local/bin/clang++

@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/sh -e
brew update

@ -5,6 +5,9 @@ free -m
env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_googletest.sh
GOOGLETEST_DIR="$(pwd)/googletest/build"
env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_recastnavigation.sh
RECASTNAVIGATION_DIR="$(pwd)/recastnavigation/build"
mkdir build
cd build
export CODE_COVERAGE=1
@ -18,4 +21,5 @@ ${ANALYZE}cmake \
-DUSE_SYSTEM_TINYXML=TRUE \
-DGTEST_ROOT="${GOOGLETEST_DIR}" \
-DGMOCK_ROOT="${GOOGLETEST_DIR}" \
-DRecastNavigation_ROOT="${RECASTNAVIGATION_DIR}" \
..

@ -304,6 +304,10 @@ if ! [ -z $UNITY_BUILD ]; then
add_cmake_opts "-DOPENMW_UNITY_BUILD=True"
fi
if [ ${BITS} -eq 64 ]; then
GENERATOR="${GENERATOR} Win64"
fi
echo
echo "==================================="
echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}"
@ -449,7 +453,7 @@ fi
else
LIB_SUFFIX="0"
fi
add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \
-DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.${LIB_SUFFIX}"
add_cmake_opts -DBoost_COMPILER="-${TOOLSET}"
@ -644,6 +648,13 @@ printf "SDL 2.0.7... "
echo Done.
}
echo
# recastnavigation
printf 'recastnavigation...'
{
env GENERATOR="${GENERATOR}" CONFIGURATION="${CONFIGURATION}" ${DEPS_INSTALL}/../../CI/build_recastnavigation.sh
add_cmake_opts -DRecastNavigation_ROOT="$(pwd)/recastnavigation/build"
}
echo
cd $DEPS_INSTALL/..
echo
echo "Setting up OpenMW build..."

@ -1,8 +1,11 @@
#!/bin/sh
#!/bin/sh -e
export CXX=clang++
export CC=clang
env GENERATOR='Unix Makefiles' CONFIGURATION=Release CI/build_recastnavigation.sh
RECASTNAVIGATION_DIR="$(pwd)/recastnavigation/build"
DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps"
QT_PATH=`brew --prefix qt`
mkdir build
@ -17,5 +20,6 @@ cmake \
-D DESIRED_QT_VERSION=5 \
-D BUILD_ESMTOOL=FALSE \
-D BUILD_MYGUI_PLUGIN=FALSE \
-D RecastNavigation_ROOT="${RECASTNAVIGATION_DIR}" \
-G"Unix Makefiles" \
..

@ -0,0 +1,17 @@
#!/bin/sh -e
git clone https://github.com/recastnavigation/recastnavigation.git
cd recastnavigation
mkdir build
cd build
cmake \
-DCMAKE_BUILD_TYPE="${CONFIGURATION}" \
-DRECASTNAVIGATION_DEMO=OFF \
-DRECASTNAVIGATION_TESTS=OFF \
-DRECASTNAVIGATION_EXAMPLES=OFF \
-DRECASTNAVIGATION_STATIC=ON \
-DCMAKE_INSTALL_PREFIX=. \
-G "${GENERATOR}" \
..
cmake --build . --config "${CONFIGURATION}"
cmake --build . --target install --config "${CONFIGURATION}"

@ -21,7 +21,7 @@ add_openmw_dir (mwrender
actors objects renderingmanager animation rotatecontroller sky npcanimation vismask
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation
bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager
renderbin actoranimation landmanager navmesh actorspaths
)
add_openmw_dir (mwinput
@ -70,7 +70,7 @@ add_openmw_dir (mwworld
)
add_openmw_dir (mwphysics
physicssystem trace collisiontype actor convert
physicssystem trace collisiontype actor convert object heightfield
)
add_openmw_dir (mwclass
@ -117,6 +117,10 @@ include_directories(
${FFmpeg_INCLUDE_DIRS}
)
find_package(RecastNavigation COMPONENTS DebugUtils Detour Recast REQUIRED)
include_directories(SYSTEM ${RecastNavigation_INCLUDE_DIRS})
target_link_libraries(openmw
${OSG_LIBRARIES}
${OPENTHREADS_LIBRARIES}
@ -133,6 +137,7 @@ target_link_libraries(openmw
${FFmpeg_LIBRARIES}
${MyGUI_LIBRARIES}
${SDL2_LIBRARY}
${RecastNavigation_LIBRARIES}
"osg-ffmpeg-videoplayer"
"oics"
components

@ -4,6 +4,7 @@
#include <vector>
#include <map>
#include <set>
#include <deque>
#include <components/esm/cellid.hpp>
@ -54,6 +55,11 @@ namespace MWMechanics
struct Movement;
}
namespace DetourNavigator
{
class Navigator;
}
namespace MWWorld
{
class CellStore;
@ -595,6 +601,13 @@ namespace MWBase
/// Preload VFX associated with this effect list
virtual void preloadEffects(const ESM::EffectList* effectList) = 0;
virtual DetourNavigator::Navigator* getNavigator() const = 0;
virtual void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const = 0;
virtual void setNavMeshNumberToRender(const std::size_t value) = 0;
};
}

@ -36,7 +36,7 @@ namespace MWMechanics
return true; //Target doesn't exist
//Set the target destination for the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
const auto dest = target.getRefData().getPosition().asVec3();
if (pathTo(actor, dest, duration, MWBase::Environment::get().getWorld()->getMaxActivationDistance())) //Stop when you get in activation range
{

@ -37,7 +37,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
if (!target)
return true;
if (!mManual && !pathTo(actor, target.getRefData().getPosition().pos, duration, mDistance))
if (!mManual && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, mDistance))
{
return false;
}

@ -128,7 +128,7 @@ namespace MWMechanics
float targetReachedTolerance = 0.0f;
if (storage.mLOS)
targetReachedTolerance = storage.mAttackRange;
bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, targetReachedTolerance);
const bool is_target_reached = pathTo(actor, target.getRefData().getPosition().asVec3(), duration, targetReachedTolerance);
if (is_target_reached) storage.mReadyToAttack = true;
}
@ -307,7 +307,7 @@ namespace MWMechanics
osg::Vec3f localPos = actor.getRefData().getPosition().asVec3();
coords.toLocal(localPos);
int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, localPos);
int closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos);
for (int i = 0; i < static_cast<int>(pathgrid->mPoints.size()); i++)
{
if (i != closestPointIndex && getPathGridGraph(storage.mCell).isPointConnected(closestPointIndex, i))
@ -359,7 +359,7 @@ namespace MWMechanics
float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
if ((dist > fFleeDistance && !storage.mLOS)
|| pathTo(actor, storage.mFleeDest, duration))
|| pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration))
{
state = AiCombatStorage::FleeState_Idle;
}

@ -100,7 +100,7 @@ namespace MWMechanics
point.mAutogenerated = 0;
point.mConnectionNum = 0;
point.mUnknown = 0;
if (pathTo(actor,point,duration)) //Returns true on path complete
if (pathTo(actor, osg::Vec3f(mX, mY, mZ), duration)) //Returns true on path complete
{
mRemainingDuration = mDuration;
return true;

@ -163,7 +163,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
}
//Set the target destination from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
const auto dest = target.getRefData().getPosition().asVec3();
short baseFollowDistance = followDistance;
short threshold = 30; // to avoid constant switching between moving/stopping
@ -196,7 +196,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
if (storage.mMoving)
{
//Check if you're far away
float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]);
float dist = distance(dest, pos.asVec3());
if (dist > 450)
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run

@ -5,6 +5,7 @@
#include <components/esm/loadcell.hpp>
#include <components/esm/loadland.hpp>
#include <components/esm/loadmgef.hpp>
#include <components/detournavigator/navigator.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
@ -25,7 +26,7 @@
MWMechanics::AiPackage::~AiPackage() {}
MWMechanics::AiPackage::AiPackage() :
MWMechanics::AiPackage::AiPackage() :
mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild
mTargetActorRefId(""),
mTargetActorId(-1),
@ -90,70 +91,67 @@ void MWMechanics::AiPackage::reset()
mTimer = AI_REACTION_TIME + 1.0f;
mIsShortcutting = false;
mShortcutProhibited = false;
mShortcutFailPos = ESM::Pathgrid::Point();
mShortcutFailPos = osg::Vec3f();
mPathFinder.clearPath();
mObstacleCheck.clear();
}
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance)
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance)
{
mTimer += duration; //Update timer
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
const auto position = actor.getRefData().getPosition().asVec3(); //position of the actor
const auto world = MWBase::Environment::get().getWorld();
{
const auto halfExtents = world->getHalfExtents(actor);
world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest);
}
/// Stops the actor when it gets too close to a unloaded cell
//... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value
//... units from player, and exterior cells are 8192 units long and wide.
//... But AI processing distance may increase in the future.
if (isNearInactiveCell(pos))
if (isNearInactiveCell(position))
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
return false;
}
// handle path building and shortcutting
ESM::Pathgrid::Point start = pos.pos;
float distToTarget = distance(start, dest);
bool isDestReached = (distToTarget <= destTolerance);
const float distToTarget = distance(position, dest);
const bool isDestReached = (distToTarget <= destTolerance);
if (!isDestReached && mTimer > AI_REACTION_TIME)
{
if (actor.getClass().isBipedal(actor))
openDoors(actor);
bool wasShortcutting = mIsShortcutting;
const bool wasShortcutting = mIsShortcutting;
bool destInLOS = false;
const MWWorld::Class& actorClass = actor.getClass();
MWBase::World* world = MWBase::Environment::get().getWorld();
// check if actor can move along z-axis
bool actorCanMoveByZ = (actorClass.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| world->isFlying(actor);
const bool actorCanMoveByZ = canActorMoveByZAxis(actor);
// Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions.
if (actorCanMoveByZ || getTypeId() != TypeIdWander)
mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first
mIsShortcutting = actorCanMoveByZ
&& shortcutPath(position, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first
if (!mIsShortcutting)
{
if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path
{
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
mPathFinder.buildPath(actor, position, dest, actor.getCell(), getPathGridGraph(actor.getCell()),
playerHalfExtents, getNavigatorFlags(actor));
mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity
if (destInLOS && mPathFinder.getPath().size() > 1)
{
// get point just before dest
std::list<ESM::Pathgrid::Point>::const_iterator pPointBeforeDest = mPathFinder.getPath().end();
--pPointBeforeDest;
--pPointBeforeDest;
auto pPointBeforeDest = mPathFinder.getPath().rbegin() + 1;
// if start point is closer to the target then last point of path (excluding target itself) then go straight on the target
if (distance(start, dest) <= distance(dest, *pPointBeforeDest))
if (distance(position, dest) <= distance(dest, *pPointBeforeDest))
{
mPathFinder.clearPath();
mPathFinder.addPointToPath(dest);
@ -163,7 +161,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr
if (!mPathFinder.getPath().empty()) //Path has points in it
{
ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path
const auto& lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path
if(distance(dest, lastPos) > 100) //End of the path is far from the destination
mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go
@ -173,41 +171,41 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr
mTimer = 0;
}
if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1])) // if path is finished
const auto pointTolerance = std::min(actor.getClass().getSpeed(actor), DEFAULT_TOLERANCE);
mPathFinder.update(position, pointTolerance, destTolerance);
if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished
{
// turn to destination point
zTurn(actor, getZAngleToPoint(start, dest));
smoothTurn(actor, getXAngleToPoint(start, dest), 0);
zTurn(actor, getZAngleToPoint(position, dest));
smoothTurn(actor, getXAngleToPoint(position, dest), 0);
return true;
}
else
{
if (mRotateOnTheRunChecks == 0
|| isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target
if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--;
}
// handle obstacles on the way
evadeObstacles(actor, duration, pos);
if (mRotateOnTheRunChecks == 0
|| isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target
if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--;
}
// turn to next path point by X,Z axes
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
smoothTurn(actor, mPathFinder.getXAngleToNext(pos.pos[0], pos.pos[1], pos.pos[2]), 0);
zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y()));
smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0);
mObstacleCheck.update(actor, duration);
// handle obstacles on the way
evadeObstacles(actor);
return false;
}
void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos)
void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor)
{
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
// check if stuck due to obstacles
if (!mObstacleCheck.check(actor, duration)) return;
if (!mObstacleCheck.isEvading()) return;
// first check if obstacle is a door
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
@ -219,13 +217,14 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur
}
else
{
mObstacleCheck.takeEvasiveAction(movement);
mObstacleCheck.takeEvasiveAction(actor.getClass().getMovementSettings(actor));
}
}
void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
{
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance();
const auto world = MWBase::Environment::get().getWorld();
static float distance = world->getMaxActivationDistance();
const MWWorld::Ptr door = getNearbyDoor(actor, distance);
if (door == MWWorld::Ptr())
@ -236,7 +235,7 @@ void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
{
if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 ))
{
MWBase::Environment::get().getWorld()->activate(door, actor);
world->activate(door, actor);
return;
}
@ -248,7 +247,7 @@ void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor)
MWWorld::Ptr keyPtr = invStore.search(keyId);
if (!keyPtr.isEmpty())
MWBase::Environment::get().getWorld()->activate(door, actor);
world->activate(door, actor);
}
}
@ -266,14 +265,15 @@ const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const
return *cache[id].get();
}
bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear)
bool MWMechanics::AiPackage::shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear)
{
if (!mShortcutProhibited || (PathFinder::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST)
if (!mShortcutProhibited || (mShortcutFailPos - startPoint).length() >= PATHFIND_SHORTCUT_RETRY_DIST)
{
// check if target is clearly visible
isPathClear = !MWBase::Environment::get().getWorld()->castRay(
static_cast<float>(startPoint.mX), static_cast<float>(startPoint.mY), static_cast<float>(startPoint.mZ),
static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), static_cast<float>(endPoint.mZ));
startPoint.x(), startPoint.y(), startPoint.z(),
endPoint.x(), endPoint.y(), endPoint.z());
if (destInLOS != nullptr) *destInLOS = isPathClear;
@ -294,21 +294,22 @@ bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint
return false;
}
bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor)
bool MWMechanics::AiPackage::checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor)
{
bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| MWBase::Environment::get().getWorld()->isFlying(actor);
const auto world = MWBase::Environment::get().getWorld();
const bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && world->isSwimming(actor))
|| world->isFlying(actor);
if (actorCanMoveByZ)
return true;
float actorSpeed = actor.getClass().getSpeed(actor);
float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
osg::Vec3f::value_type distToTarget = osg::Vec3f(static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), 0).length();
const float actorSpeed = actor.getClass().getSpeed(actor);
const float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
const float distToTarget = osg::Vec2f(endPoint.x(), endPoint.y()).length();
float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2;
const float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2;
bool isClear = checkWayIsClear(PathFinder::MakeOsgVec3(startPoint), PathFinder::MakeOsgVec3(endPoint), offsetXY);
const bool isClear = checkWayIsClear(startPoint, endPoint, offsetXY);
// update shortcut prohibit state
if (isClear)
@ -316,12 +317,12 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point&
if (mShortcutProhibited)
{
mShortcutProhibited = false;
mShortcutFailPos = ESM::Pathgrid::Point();
mShortcutFailPos = osg::Vec3f();
}
}
if (!isClear)
{
if (mShortcutFailPos.mX == 0 && mShortcutFailPos.mY == 0 && mShortcutFailPos.mZ == 0)
if (mShortcutFailPos == osg::Vec3f())
{
mShortcutProhibited = true;
mShortcutFailPos = startPoint;
@ -331,9 +332,11 @@ bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point&
return isClear;
}
bool MWMechanics::AiPackage::doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell)
bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell)
{
return mPathFinder.getPath().empty() || (distance(mPathFinder.getPath().back(), newDest) > 10) || mPathFinder.getPathCell() != currentCell;
return mPathFinder.getPath().empty()
|| (distance(mPathFinder.getPath().back(), newDest) > 10)
|| mPathFinder.getPathCell() != currentCell;
}
bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
@ -343,14 +346,13 @@ bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
|| (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75);
}
bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position)
{
const ESM::Cell* playerCell(getPlayer().getCell()->getCell());
if (playerCell->isExterior())
{
// get actor's distance from origin of center cell
osg::Vec3f actorOffset(actorPos.asVec3());
CoordinateConverter(playerCell).toLocal(actorOffset);
CoordinateConverter(playerCell).toLocal(position);
// currently assumes 3 x 3 grid for exterior cells, with player at center cell.
// ToDo: (Maybe) use "exterior cell load distance" setting to get count of actual active cells
@ -358,8 +360,8 @@ bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
const float distanceFromEdge = 200.0;
float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge;
float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge;
return (actorOffset[0] < minThreshold) || (maxThreshold < actorOffset[0])
|| (actorOffset[1] < minThreshold) || (maxThreshold < actorOffset[1]);
return (position.x() < minThreshold) || (maxThreshold < position.x())
|| (position.y() < minThreshold) || (maxThreshold < position.y());
}
else
{
@ -367,7 +369,7 @@ bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
}
}
bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest)
bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest)
{
// get actor's shortest radius for moving in circle
float speed = actor.getClass().getSpeed(actor);
@ -381,17 +383,40 @@ bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& act
osg::Vec3f radiusDir = dir ^ osg::Z_AXIS; // radius is perpendicular to a tangent
radiusDir.normalize();
radiusDir *= radius;
// pick up the nearest center candidate
osg::Vec3f dest_ = PathFinder::MakeOsgVec3(dest);
osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
osg::Vec3f center1 = pos - radiusDir;
osg::Vec3f center2 = pos + radiusDir;
osg::Vec3f center = (center1 - dest_).length2() < (center2 - dest_).length2() ? center1 : center2;
osg::Vec3f center = (center1 - dest).length2() < (center2 - dest).length2() ? center1 : center2;
float distToDest = (center - dest_).length();
float distToDest = (center - dest).length();
// if pathpoint is reachable for the actor rotating on the run:
// no points of actor's circle should be farther from the center than destination point
return (radius <= distToDest);
}
DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const
{
const auto& actorClass = actor.getClass();
DetourNavigator::Flags result = DetourNavigator::Flag_none;
if (actorClass.isPureWaterCreature(actor) || (getTypeId() != TypeIdWander && actorClass.canSwim(actor)))
result |= DetourNavigator::Flag_swim;
if (actorClass.canWalk(actor))
result |= DetourNavigator::Flag_walk;
if (actorClass.isBipedal(actor) && getTypeId() != TypeIdWander)
result |= DetourNavigator::Flag_openDoor;
return result;
}
bool MWMechanics::AiPackage::canActorMoveByZAxis(const MWWorld::Ptr& actor) const
{
const auto world = MWBase::Environment::get().getWorld();
const auto& actorClass = actor.getClass();
return (actorClass.canSwim(actor) && world->isSwimming(actor)) || world->isFlying(actor);
}

@ -7,6 +7,8 @@
#include "obstacle.hpp"
#include "aistate.hpp"
#include <boost/optional.hpp>
namespace MWWorld
{
class Ptr;
@ -105,29 +107,35 @@ namespace MWMechanics
bool isTargetMagicallyHidden(const MWWorld::Ptr& target);
/// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing.
static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest);
static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest);
protected:
/// Handles path building and shortcutting with obstacles avoiding
/** \return If the actor has arrived at his destination **/
bool pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance = 0.0f);
bool pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& dest, float duration, float destTolerance = 0.0f);
/// Check if there aren't any obstacles along the path to make shortcut possible
/// If a shortcut is possible then path will be cleared and filled with the destination point.
/// \param destInLOS If not nullptr function will return ray cast check result
/// \return If can shortcut the path
bool shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS, bool isPathClear);
bool shortcutPath(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor,
bool *destInLOS, bool isPathClear);
/// Check if the way to the destination is clear, taking into account actor speed
bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor);
bool checkWayIsClearForActor(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::Ptr& actor);
bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::CellStore* currentCell);
virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell);
void evadeObstacles(const MWWorld::Ptr& actor);
void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos);
void openDoors(const MWWorld::Ptr& actor);
const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell);
DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const;
bool canActorMoveByZAxis(const MWWorld::Ptr& actor) const;
// TODO: all this does not belong here, move into temporary storage
PathFinder mPathFinder;
ObstacleCheck mObstacleCheck;
@ -137,16 +145,14 @@ namespace MWMechanics
std::string mTargetActorRefId;
mutable int mTargetActorId;
osg::Vec3f mLastActorPos;
short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility
bool mIsShortcutting; // if shortcutting at the moment
bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt
ESM::Pathgrid::Point mShortcutFailPos; // position of last shortcut fail
osg::Vec3f mShortcutFailPos; // position of last shortcut fail
private:
bool isNearInactiveCell(const ESM::Position& actorPos);
bool isNearInactiveCell(osg::Vec3f position);
};
}

@ -49,13 +49,13 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing);
//Set the target desition from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
const auto dest = target.getRefData().getPosition().asVec3();
ESM::Position aPos = actor.getRefData().getPosition();
float pathTolerance = 100.0;
if (pathTo(actor, dest, duration, pathTolerance) &&
std::abs(dest.mZ - aPos.pos[2]) < pathTolerance) // check the true distance in case the target is far away in Z-direction
std::abs(dest.z() - aPos.pos[2]) < pathTolerance) // check the true distance in case the target is far away in Z-direction
{
target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached
return true;

@ -58,8 +58,7 @@ namespace MWMechanics
// Unfortunately, with vanilla assets destination is sometimes blocked by other actor.
// If we got close to target, check for actors nearby. If they are, finish AI package.
int destinationTolerance = 64;
ESM::Pathgrid::Point dest(static_cast<int>(mX), static_cast<int>(mY), static_cast<int>(mZ));
if (distance(pos.pos, dest) <= destinationTolerance)
if (distance(pos.asVec3(), osg::Vec3f(mX, mY, mZ)) <= destinationTolerance)
{
std::vector<MWWorld::Ptr> targetActors;
std::pair<MWWorld::Ptr, osg::Vec3f> result = MWBase::Environment::get().getWorld()->getHitContact(actor, destinationTolerance, targetActors);
@ -71,7 +70,7 @@ namespace MWMechanics
}
}
if (pathTo(actor, dest, duration))
if (pathTo(actor, osg::Vec3f(mX, mY, mZ), duration))
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
return true;

@ -50,7 +50,8 @@ namespace MWMechanics
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle),
mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0))
mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)),
mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)), mUsePathgrid(false)
{
mIdle.resize(8, 0);
init();
@ -151,16 +152,24 @@ namespace MWMechanics
// rebuild a path to it
if (!mPathFinder.isPathConstructed() && mHasDestination)
{
ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination));
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mUsePathgrid)
{
mPathFinder.buildPathByPathgrid(pos.asVec3(), mDestination, actor.getCell(),
getPathGridGraph(actor.getCell()));
}
else
{
const auto world = MWBase::Environment::get().getWorld();
const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(),
getPathGridGraph(actor.getCell()), playerHalfExtents, getNavigatorFlags(actor));
}
if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking);
}
doPerFrameActionsForState(actor, duration, storage, pos);
doPerFrameActionsForState(actor, duration, storage);
playIdleDialogueRandomly(actor);
@ -169,14 +178,14 @@ namespace MWMechanics
if (AI_REACTION_TIME <= lastReaction)
{
lastReaction = 0;
return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration);
return reactionTimeActions(actor, storage, currentCell, cellChange, pos);
}
else
return false;
}
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage,
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration)
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos)
{
if (mDistance <= 0)
storage.mCanWanderAlongPathGrid = false;
@ -226,7 +235,7 @@ namespace MWMechanics
}
// If Wandering manually and hit an obstacle, stop
if (storage.mIsWanderingManually && mObstacleCheck.check(actor, duration, 2.0f)) {
if (storage.mIsWanderingManually && mObstacleCheck.isEvading()) {
completeManualWalking(actor, storage);
}
@ -251,7 +260,9 @@ namespace MWMechanics
setPathToAnAllowedNode(actor, storage, pos);
}
}
} else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) {
}
else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted())
{
completeManualWalking(actor, storage);
}
@ -292,11 +303,9 @@ namespace MWMechanics
* Commands actor to walk to a random location near original spawn location.
*/
void AiWander::wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance) {
const ESM::Pathgrid::Point currentPosition = actor.getRefData().getPosition().pos;
const osg::Vec3f currentPositionVec3f = osg::Vec3f(currentPosition.mX, currentPosition.mY, currentPosition.mZ);
const auto currentPosition = actor.getRefData().getPosition().asVec3();
std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here
ESM::Pathgrid::Point destinationPosition;
bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
do {
// Determine a random location within radius of original position
@ -305,19 +314,24 @@ namespace MWMechanics
const float destinationX = mInitialActorPosition.x() + wanderRadius * std::cos(randomDirection);
const float destinationY = mInitialActorPosition.y() + wanderRadius * std::sin(randomDirection);
const float destinationZ = mInitialActorPosition.z();
destinationPosition = ESM::Pathgrid::Point(destinationX, destinationY, destinationZ);
const osg::Vec3f destinationPosition(destinationX, destinationY, destinationZ);
mDestination = osg::Vec3f(destinationX, destinationY, destinationZ);
// Check if land creature will walk onto water or if water creature will swim onto land
if ((!isWaterCreature && !destinationIsAtWater(actor, mDestination)) ||
(isWaterCreature && !destinationThroughGround(currentPositionVec3f, mDestination))) {
mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), getPathGridGraph(actor.getCell()));
(isWaterCreature && !destinationThroughGround(currentPosition, mDestination)))
{
const auto world = MWBase::Environment::get().getWorld();;
const auto playerHalfExtents = world->getHalfExtents(world->getPlayerPtr());
mPathFinder.buildPath(actor, currentPosition, destinationPosition, actor.getCell(),
getPathGridGraph(actor.getCell()), playerHalfExtents, getNavigatorFlags(actor));
mPathFinder.addPointToPath(destinationPosition);
if (mPathFinder.isPathConstructed())
{
storage.setState(AiWanderStorage::Wander_Walking, true);
mHasDestination = true;
mUsePathgrid = false;
}
return;
}
@ -348,7 +362,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow);
}
void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos)
void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{
switch (storage.mState)
{
@ -357,7 +371,7 @@ namespace MWMechanics
break;
case AiWanderStorage::Wander_Walking:
onWalkingStatePerFrameActions(actor, duration, storage, pos);
onWalkingStatePerFrameActions(actor, duration, storage);
break;
case AiWanderStorage::Wander_ChooseAction:
@ -413,11 +427,10 @@ namespace MWMechanics
}
}
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor,
float duration, AiWanderStorage& storage, ESM::Position& pos)
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{
// Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed()) || pathTo(actor, ESM::Pathgrid::Point(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE))
if ((!mPathFinder.isPathConstructed()) || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE))
{
stopWalking(actor, storage);
storage.setState(AiWanderStorage::Wander_ChooseAction);
@ -425,7 +438,7 @@ namespace MWMechanics
else
{
// have not yet reached the destination
evadeObstacles(actor, storage, duration, pos);
evadeObstacles(actor, storage);
}
}
@ -456,7 +469,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow);
}
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos)
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{
if (mObstacleCheck.isEvading())
{
@ -592,15 +605,17 @@ namespace MWMechanics
ToWorldCoordinates(dest, storage.mCell->getCell());
// actor position is already in world coordinates
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos));
const auto start = actorPos.asVec3();
// don't take shortcuts for wandering
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
const auto destVec3f = PathFinder::makeOsgVec3(dest);
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mPathFinder.isPathConstructed())
{
mDestination = osg::Vec3f(dest.mX, dest.mY, dest.mZ);
mDestination = destVec3f;
mHasDestination = true;
mUsePathgrid = true;
// Remove this node as an option and add back the previously used node (stops NPC from picking the same node):
ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
@ -631,15 +646,15 @@ namespace MWMechanics
// Every now and then check whether one of the doors is opened. (maybe
// at the end of playing idle?) If the door is opened then re-calculate
// allowed nodes starting from the spawn point.
std::list<ESM::Pathgrid::Point> paths = pathfinder.getPath();
auto paths = pathfinder.getPath();
while(paths.size() >= 2)
{
ESM::Pathgrid::Point pt = paths.back();
const auto pt = paths.back();
for(unsigned int j = 0; j < nodes.size(); j++)
{
// FIXME: doesn't handle a door with the same X/Y
// coordinates but with a different Z
if(nodes[j].mX == pt.mX && nodes[j].mY == pt.mY)
if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5)
{
nodes.erase(nodes.begin() + j);
break;
@ -731,7 +746,7 @@ namespace MWMechanics
ESM::Pathgrid::Point worldDest = dest;
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::MakeOsgVec3(worldDest), 60);
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60);
// add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied)
@ -752,18 +767,18 @@ namespace MWMechanics
ESM::Pathgrid::Point connDest = points[randomIndex];
// add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::MakeOsgVec3(connDest) - PathFinder::MakeOsgVec3(dest);
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest);
float length = dir.length();
dir.normalize();
for (int j = 1; j <= 3; j++)
{
// move for 5-15% towards random neighboring node
dest = PathFinder::MakePathgridPoint(PathFinder::MakeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
dest = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = dest;
ToWorldCoordinates(worldDest, actor.getCell()->getCell());
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::MakeOsgVec3(worldDest), 60);
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(PathFinder::makeOsgVec3(worldDest), 60);
if (!isOccupied)
break;
@ -799,7 +814,7 @@ namespace MWMechanics
const ESM::Pathgrid *pathgrid =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*currentCell->getCell());
int index = PathFinder::GetClosestPoint(pathgrid, PathFinder::MakeOsgVec3(dest));
int index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest));
getPathGridGraph(currentCell).getNeighbouringPoints(index, points);
}
@ -832,7 +847,7 @@ namespace MWMechanics
CoordinateConverter(cell).toLocal(npcPos);
// Find closest pathgrid point
int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, npcPos);
int closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos);
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
// and if the point is connected to the closest current point
@ -840,7 +855,7 @@ namespace MWMechanics
int pointIndex = 0;
for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
{
osg::Vec3f nodePos(PathFinder::MakeOsgVec3(pathgrid->mPoints[counter]));
osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter]));
if((npcPos - nodePos).length2() <= mDistance * mDistance &&
getPathGridGraph(cellStore).isPointConnected(closestPointIndex, counter))
{
@ -867,7 +882,7 @@ namespace MWMechanics
// 2. Partway along the path between the point and its connected points.
void AiWander::AddNonPathGridAllowedPoints(osg::Vec3f npcPos, const ESM::Pathgrid * pathGrid, int pointIndex, AiWanderStorage& storage)
{
storage.mAllowedNodes.push_back(PathFinder::MakePathgridPoint(npcPos));
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(npcPos));
for (std::vector<ESM::Pathgrid::Edge>::const_iterator it = pathGrid->mEdges.begin(); it != pathGrid->mEdges.end(); ++it)
{
if (it->mV0 == pointIndex)
@ -879,8 +894,8 @@ namespace MWMechanics
void AiWander::AddPointBetweenPathGridPoints(const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage)
{
osg::Vec3f vectorStart = PathFinder::MakeOsgVec3(start);
osg::Vec3f delta = PathFinder::MakeOsgVec3(end) - vectorStart;
osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start);
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart;
float length = delta.length();
delta.normalize();
@ -889,7 +904,7 @@ namespace MWMechanics
// must not travel longer than distance between waypoints or NPC goes past waypoint
distance = std::min(distance, static_cast<int>(length));
delta *= distance;
storage.mAllowedNodes.push_back(PathFinder::MakePathgridPoint(vectorStart + delta));
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta));
}
void AiWander::SetCurrentNodeToClosestAllowedNode(const osg::Vec3f& npcPos, AiWanderStorage& storage)
@ -898,7 +913,7 @@ namespace MWMechanics
unsigned int index = 0;
for (unsigned int counterThree = 0; counterThree < storage.mAllowedNodes.size(); counterThree++)
{
osg::Vec3f nodePos(PathFinder::MakeOsgVec3(storage.mAllowedNodes[counterThree]));
osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[counterThree]));
float tempDist = (npcPos - nodePos).length2();
if (tempDist < distanceToClosestNode)
{

@ -137,15 +137,15 @@ namespace MWMechanics
short unsigned getRandomIdle();
void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos);
void playGreetingIfPlayerGetsTooClose(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos);
void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void playIdleDialogueRandomly(const MWWorld::Ptr& actor);
void turnActorToFacePlayer(const osg::Vec3f& actorPosition, const osg::Vec3f& playerPosition, AiWanderStorage& storage);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage);
void onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage);
void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos);
void onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage);
void onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage);
bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage,
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration);
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos);
bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
@ -164,6 +164,7 @@ namespace MWMechanics
bool mHasDestination;
osg::Vec3f mDestination;
bool mUsePathgrid;
void getNeighbouringNodes(ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points);

@ -33,11 +33,12 @@ namespace MWMechanics
point.y() -= static_cast<float>(mCellY);
}
osg::Vec3f CoordinateConverter::toLocalVec3(const ESM::Pathgrid::Point& point)
osg::Vec3f CoordinateConverter::toLocalVec3(const osg::Vec3f& point)
{
return osg::Vec3f(
static_cast<float>(point.mX - mCellX),
static_cast<float>(point.mY - mCellY),
static_cast<float>(point.mZ));
point.x() - static_cast<float>(mCellX),
point.y() - static_cast<float>(mCellY),
point.z()
);
}
}

@ -26,7 +26,7 @@ namespace MWMechanics
/// in-place conversion from world to local
void toLocal(osg::Vec3f& point);
osg::Vec3f toLocalVec3(const ESM::Pathgrid::Point& point);
osg::Vec3f toLocalVec3(const osg::Vec3f& point);
private:
int mCellX;

@ -96,11 +96,6 @@ namespace MWMechanics
mEvadeDuration = 0;
}
bool ObstacleCheck::isNormalState() const
{
return mWalkState == State_Norm;
}
bool ObstacleCheck::isEvading() const
{
return mWalkState == State_Evade;
@ -128,7 +123,7 @@ namespace MWMechanics
* u = how long to move sideways
*
*/
bool ObstacleCheck::check(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance)
void ObstacleCheck::update(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance)
{
const MWWorld::Class& cls = actor.getClass();
ESM::Position pos = actor.getRefData().getPosition();
@ -180,9 +175,7 @@ namespace MWMechanics
case State_Evade:
{
mEvadeDuration += duration;
if(mEvadeDuration < DURATION_TO_EVADE)
return true;
else
if(mEvadeDuration >= DURATION_TO_EVADE)
{
// tried to evade, assume all is ok and start again
mWalkState = State_Norm;
@ -191,10 +184,9 @@ namespace MWMechanics
}
/* NO DEFAULT CASE */
}
return false; // no obstacles to evade (yet)
}
void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement)
void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement) const
{
actorMovement.mPosition[0] = evadeDirections[mEvadeDirectionIndex][0];
actorMovement.mPosition[1] = evadeDirections[mEvadeDirectionIndex][1];

@ -27,15 +27,13 @@ namespace MWMechanics
// Clear the timers and set the state machine to default
void clear();
bool isNormalState() const;
bool isEvading() const;
// Returns true if there is an obstacle and an evasive action
// should be taken
bool check(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance = 1.0f);
// Updates internal state, call each frame for moving actor
void update(const MWWorld::Ptr& actor, float duration, float scaleMinimumDistance = 1.0f);
// change direction to try to fix "stuck" actor
void takeEvasiveAction(MWMechanics::Movement& actorMovement);
void takeEvasiveAction(MWMechanics::Movement& actorMovement) const;
private:

@ -1,13 +1,20 @@
#include "pathfinding.hpp"
#include <iterator>
#include <limits>
#include <components/detournavigator/exceptions.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/debug/debuglog.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwphysics/collisiontype.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "pathgrid.hpp"
#include "coordinateconverter.hpp"
@ -29,7 +36,7 @@ namespace
// points to a quadtree may help
for(unsigned int counter = 0; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = MWMechanics::PathFinder::DistanceSquared(grid->mPoints[counter], pos);
float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos);
if (potentialDistBetween < closestDistanceReachable)
{
// found a closer one
@ -57,56 +64,19 @@ namespace
(closestReachableIndex, closestReachableIndex == closestIndex);
}
}
namespace MWMechanics
{
float sqrDistanceIgnoreZ(const ESM::Pathgrid::Point& point, float x, float y)
{
x -= point.mX;
y -= point.mY;
return (x * x + y * y);
}
float distance(const ESM::Pathgrid::Point& point, float x, float y, float z)
{
x -= point.mX;
y -= point.mY;
z -= point.mZ;
return sqrt(x * x + y * y + z * z);
}
float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b)
{
float x = static_cast<float>(a.mX - b.mX);
float y = static_cast<float>(a.mY - b.mY);
float z = static_cast<float>(a.mZ - b.mZ);
return sqrt(x * x + y * y + z * z);
}
float getZAngleToDir(const osg::Vec3f& dir)
{
return std::atan2(dir.x(), dir.y());
}
float getXAngleToDir(const osg::Vec3f& dir)
{
float dirLen = dir.length();
return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0;
}
float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest)
float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs)
{
osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin);
return getZAngleToDir(dir);
return (lhs - rhs).length2();
}
float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest)
float sqrDistanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs)
{
osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin);
return getXAngleToDir(dir);
return sqrDistance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y()));
}
}
namespace MWMechanics
{
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY)
{
osg::Vec3f dir = to - from;
@ -121,18 +91,6 @@ namespace MWMechanics
return (std::abs(from.z() - h) <= PATHFIND_Z_REACH);
}
PathFinder::PathFinder()
: mPathgrid(nullptr)
, mCell(nullptr)
{
}
void PathFinder::clearPath()
{
if(!mPath.empty())
mPath.clear();
}
/*
* NOTE: This method may fail to find a path. The caller must check the
* result before using it. If there is no path the AI routies need to
@ -150,7 +108,7 @@ namespace MWMechanics
* pathgrid point (e.g. wander) then it may be worth while to call
* pop_back() to remove the redundant entry.
*
* NOTE: coordinates must be converted prior to calling GetClosestPoint()
* NOTE: coordinates must be converted prior to calling getClosestPoint()
*
* |
* | cell
@ -169,52 +127,44 @@ namespace MWMechanics
* j = @.x in local coordinates (i.e. within the cell)
* k = @.x in world coordinates
*/
void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph)
void PathFinder::buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const PathgridGraph& pathgridGraph, std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{
mPath.clear();
// TODO: consider removing mCell / mPathgrid in favor of mPathgridGraph
if(mCell != cell || !mPathgrid)
{
mCell = cell;
mPathgrid = pathgridGraph.getPathgrid();
}
const auto pathgrid = pathgridGraph.getPathgrid();
// Refer to AiWander reseach topic on openmw forums for some background.
// Maybe there is no pathgrid for this cell. Just go to destination and let
// physics take care of any blockages.
if(!mPathgrid || mPathgrid->mPoints.empty())
if(!pathgrid || pathgrid->mPoints.empty())
{
mPath.push_back(endPoint);
*out++ = endPoint;
return;
}
// NOTE: GetClosestPoint expects local coordinates
// NOTE: getClosestPoint expects local coordinates
CoordinateConverter converter(mCell->getCell());
// NOTE: It is possible that GetClosestPoint returns a pathgrind point index
// NOTE: It is possible that getClosestPoint returns a pathgrind point index
// that is unreachable in some situations. e.g. actor is standing
// outside an area enclosed by walls, but there is a pathgrid
// point right behind the wall that is closer than any pathgrid
// point outside the wall
osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint));
int startNode = GetClosestPoint(mPathgrid, startPointInLocalCoords);
int startNode = getClosestPoint(pathgrid, startPointInLocalCoords);
osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint));
std::pair<int, bool> endNode = getClosestReachablePoint(mPathgrid, &pathgridGraph,
std::pair<int, bool> endNode = getClosestReachablePoint(pathgrid, &pathgridGraph,
endPointInLocalCoords,
startNode);
// if it's shorter for actor to travel from start to end, than to travel from either
// start or end to nearest pathgrid point, just travel from start to end.
float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2();
float endTolastNodeLength2 = DistanceSquared(mPathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = DistanceSquared(mPathgrid->mPoints[startNode], startPointInLocalCoords);
float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2))
{
mPath.push_back(endPoint);
*out++ = endPoint;
return;
}
@ -225,21 +175,21 @@ namespace MWMechanics
// nodes are the same
if(startNode == endNode.first)
{
ESM::Pathgrid::Point temp(mPathgrid->mPoints[startNode]);
ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]);
converter.toWorld(temp);
mPath.push_back(temp);
*out++ = makeOsgVec3(temp);
}
else
{
mPath = pathgridGraph.aStarSearch(startNode, endNode.first);
auto path = pathgridGraph.aStarSearch(startNode, endNode.first);
// If nearest path node is in opposite direction from second, remove it from path.
// Especially useful for wandering actors, if the nearest node is blocked for some reason.
if (mPath.size() > 1)
if (path.size() > 1)
{
ESM::Pathgrid::Point secondNode = *(++mPath.begin());
osg::Vec3f firstNodeVec3f = MakeOsgVec3(mPathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = MakeOsgVec3(secondNode);
ESM::Pathgrid::Point secondNode = *(++path.begin());
osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode);
osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f;
osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f;
if (toSecondNodeVec3f * toStartPointVec3f > 0)
@ -248,23 +198,25 @@ namespace MWMechanics
converter.toWorld(temp);
// Add Z offset since path node can overlap with other objects.
// Also ignore doors in raytesting.
int mask = MWPhysics::CollisionType_World;
const int mask = MWPhysics::CollisionType_World;
bool isPathClear = !MWBase::Environment::get().getWorld()->castRay(
startPoint.mX, startPoint.mY, startPoint.mZ+16, temp.mX, temp.mY, temp.mZ+16, mask);
startPoint.x(), startPoint.y(), startPoint.z() + 16, temp.mX, temp.mY, temp.mZ + 16, mask);
if (isPathClear)
mPath.pop_front();
path.pop_front();
}
}
// convert supplied path to world coordinates
for (std::list<ESM::Pathgrid::Point>::iterator iter(mPath.begin()); iter != mPath.end(); ++iter)
{
converter.toWorld(*iter);
}
std::transform(path.begin(), path.end(), out,
[&] (ESM::Pathgrid::Point& point)
{
converter.toWorld(point);
return makeOsgVec3(point);
});
}
// If endNode found is NOT the closest PathGrid point to the endPoint,
// assume endPoint is not reachable from endNode. In which case,
// assume endPoint is not reachable from endNode. In which case,
// path ends at endNode.
//
// So only add the destination (which may be different to the closest
@ -276,7 +228,7 @@ namespace MWMechanics
//
// The AI routines will have to deal with such situations.
if(endNode.second)
mPath.push_back(endPoint);
*out++ = endPoint;
}
float PathFinder::getZAngleToNext(float x, float y) const
@ -286,9 +238,9 @@ namespace MWMechanics
if(mPath.empty())
return 0.;
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
float directionX = nextPoint.mX - x;
float directionY = nextPoint.mY - y;
const auto& nextPoint = mPath.front();
const float directionX = nextPoint.x() - x;
const float directionY = nextPoint.y() - y;
return std::atan2(directionX, directionY);
}
@ -300,62 +252,64 @@ namespace MWMechanics
if(mPath.empty())
return 0.;
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
osg::Vec3f dir = MakeOsgVec3(nextPoint) - osg::Vec3f(x,y,z);
const osg::Vec3f dir = mPath.front() - osg::Vec3f(x, y, z);
return getXAngleToDir(dir);
}
bool PathFinder::checkPathCompleted(float x, float y, float tolerance)
void PathFinder::update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance)
{
if(mPath.empty())
return true;
if (mPath.empty())
return;
const ESM::Pathgrid::Point& nextPoint = *mPath.begin();
if (sqrDistanceIgnoreZ(nextPoint, x, y) < tolerance*tolerance)
{
const auto tolerance = mPath.size() > 1 ? pointTolerance : destinationTolerance;
if (sqrDistanceIgnoreZ(mPath.front(), position) < tolerance * tolerance)
mPath.pop_front();
if(mPath.empty())
{
return true;
}
}
}
void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph)
{
mPath.clear();
mCell = cell;
return false;
buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath));
mConstructed = true;
}
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents,
const DetourNavigator::Flags flags)
{
mPath.clear();
mCell = cell;
buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, std::back_inserter(mPath));
if (mPath.empty())
buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath));
mConstructed = true;
}
// see header for the rationale
void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const MWMechanics::PathgridGraph& pathgridGraph)
void PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags,
std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{
if (mPath.size() < 2)
try
{
// if path has one point, then it's the destination.
// don't need to worry about bad path for this case
buildPath(startPoint, endPoint, cell, pathgridGraph);
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
navigator->findPath(halfExtents, startPoint, endPoint, flags, out);
}
else
catch (const DetourNavigator::NavigatorException& exception)
{
const ESM::Pathgrid::Point oldStart(*getPath().begin());
buildPath(startPoint, endPoint, cell, pathgridGraph);
if (mPath.size() >= 2)
{
// if 2nd waypoint of new path == 1st waypoint of old,
// delete 1st waypoint of new path.
std::list<ESM::Pathgrid::Point>::iterator iter = ++mPath.begin();
if (iter->mX == oldStart.mX
&& iter->mY == oldStart.mY
&& iter->mZ == oldStart.mZ)
{
mPath.pop_front();
}
}
DetourNavigator::log("PathFinder::buildPathByNavigator navigator exception: ", exception.what());
Log(Debug::Verbose) << "Build path by navigator exception: \"" << exception.what()
<< "\" for \"" << actor.getClass().getName(actor) << "\" (" << actor.getBase()
<< ") from " << startPoint << " to " << endPoint << " with flags ("
<< DetourNavigator::WriteFlags {flags} << ")";
}
}
const MWWorld::CellStore* PathFinder::getPathCell() const
{
return mCell;
}
}

@ -1,33 +1,57 @@
#ifndef GAME_MWMECHANICS_PATHFINDING_H
#define GAME_MWMECHANICS_PATHFINDING_H
#include <list>
#include <deque>
#include <cassert>
#include <iterator>
#include <components/detournavigator/flags.hpp>
#include <components/esm/defs.hpp>
#include <components/esm/loadpgrd.hpp>
namespace MWWorld
{
class CellStore;
class ConstPtr;
}
namespace MWMechanics
{
class PathgridGraph;
float distance(const ESM::Pathgrid::Point& point, float x, float y, float);
float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b);
float getZAngleToDir(const osg::Vec3f& dir);
float getXAngleToDir(const osg::Vec3f& dir);
float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest);
float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest);
inline float distance(const osg::Vec3f& lhs, const osg::Vec3f& rhs)
{
return (lhs - rhs).length();
}
inline float getZAngleToDir(const osg::Vec3f& dir)
{
return std::atan2(dir.x(), dir.y());
}
inline float getXAngleToDir(const osg::Vec3f& dir)
{
float dirLen = dir.length();
return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0;
}
inline float getZAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest)
{
return getZAngleToDir(dest - origin);
}
inline float getXAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest)
{
return getXAngleToDir(dest - origin);
}
const float PATHFIND_Z_REACH = 50.0f;
const float PATHFIND_Z_REACH = 50.0f;
//static const float sMaxSlope = 49.0f; // duplicate as in physicssystem
// distance after which actor (failed previously to shortcut) will try again
const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f;
const float DEFAULT_TOLERANCE = 32.0f;
// cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target;
// magnitude of pits/obstacles is defined by PATHFIND_Z_REACH
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY);
@ -35,31 +59,33 @@ namespace MWMechanics
class PathFinder
{
public:
PathFinder();
static const int PathTolerance = 32;
static float sgn(float val)
PathFinder()
: mConstructed(false)
, mCell(nullptr)
{
if(val > 0)
return 1.0;
return -1.0;
}
static int sgn(int a)
void clearPath()
{
if(a > 0)
return 1;
return -1;
mConstructed = false;
mPath.clear();
mCell = nullptr;
}
void clearPath();
void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& halfExtents,
const DetourNavigator::Flags flags);
void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
/// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance);
bool checkPathCompleted(float x, float y, float tolerance = PathTolerance);
///< \Returns true if we are within \a tolerance units of the last path point.
bool checkPathCompleted() const
{
return mConstructed && mPath.empty();
}
/// In radians
float getZAngleToNext(float x, float y) const;
@ -68,60 +94,54 @@ namespace MWMechanics
bool isPathConstructed() const
{
return !mPath.empty();
return mConstructed && !mPath.empty();
}
int getPathSize() const
std::size_t getPathSize() const
{
return mPath.size();
}
const std::list<ESM::Pathgrid::Point>& getPath() const
const std::deque<osg::Vec3f>& getPath() const
{
return mPath;
}
const MWWorld::CellStore* getPathCell() const;
/** Synchronize new path with old one to avoid visiting 1 waypoint 2 times
@note
BuildPath() takes closest PathGrid point to NPC as first point of path.
This is undesirable if NPC has just passed a Pathgrid point, as this
makes the 2nd point of the new path == the 1st point of old path.
Which results in NPC "running in a circle" back to the just passed waypoint.
*/
void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
const MWWorld::CellStore* getPathCell() const
{
return mCell;
}
void addPointToPath(const ESM::Pathgrid::Point &point)
void addPointToPath(const osg::Vec3f& point)
{
mConstructed = true;
mPath.push_back(point);
}
/// utility function to convert a osg::Vec3f to a Pathgrid::Point
static ESM::Pathgrid::Point MakePathgridPoint(const osg::Vec3f& v)
static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v)
{
return ESM::Pathgrid::Point(static_cast<int>(v[0]), static_cast<int>(v[1]), static_cast<int>(v[2]));
}
/// utility function to convert an ESM::Position to a Pathgrid::Point
static ESM::Pathgrid::Point MakePathgridPoint(const ESM::Position& p)
static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p)
{
return ESM::Pathgrid::Point(static_cast<int>(p.pos[0]), static_cast<int>(p.pos[1]), static_cast<int>(p.pos[2]));
}
static osg::Vec3f MakeOsgVec3(const ESM::Pathgrid::Point& p)
static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p)
{
return osg::Vec3f(static_cast<float>(p.mX), static_cast<float>(p.mY), static_cast<float>(p.mZ));
}
// Slightly cheaper version for comparisons.
// Caller needs to be careful for very short distances (i.e. less than 1)
// or when accumuating the results i.e. (a + b)^2 != a^2 + b^2
//
static float DistanceSquared(ESM::Pathgrid::Point point, const osg::Vec3f& pos)
static float distanceSquared(ESM::Pathgrid::Point point, const osg::Vec3f& pos)
{
return (MWMechanics::PathFinder::MakeOsgVec3(point) - pos).length2();
return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position
@ -130,18 +150,18 @@ namespace MWMechanics
//
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
static int GetClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
static int getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
{
assert(grid && !grid->mPoints.empty());
float distanceBetween = DistanceSquared(grid->mPoints[0], pos);
float distanceBetween = distanceSquared(grid->mPoints[0], pos);
int closestIndex = 0;
// TODO: if this full scan causes performance problems mapping pathgrid
// points to a quadtree may help
for(unsigned int counter = 1; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = DistanceSquared(grid->mPoints[counter], pos);
float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos);
if(potentialDistBetween < distanceBetween)
{
distanceBetween = potentialDistBetween;
@ -153,10 +173,17 @@ namespace MWMechanics
}
private:
std::list<ESM::Pathgrid::Point> mPath;
bool mConstructed;
std::deque<osg::Vec3f> mPath;
const ESM::Pathgrid *mPathgrid;
const MWWorld::CellStore* mCell;
void buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const PathgridGraph& pathgridGraph, std::back_insert_iterator<std::deque<osg::Vec3f>> out);
void buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const osg::Vec3f& halfExtents, const DetourNavigator::Flags flags,
std::back_insert_iterator<std::deque<osg::Vec3f>> out);
};
}

@ -257,10 +257,9 @@ namespace MWMechanics
* pathgrid points form (currently they are converted to world
* coordinates). Essentially trading speed w/ memory.
*/
std::list<ESM::Pathgrid::Point> PathgridGraph::aStarSearch(const int start,
const int goal) const
std::deque<ESM::Pathgrid::Point> PathgridGraph::aStarSearch(const int start, const int goal) const
{
std::list<ESM::Pathgrid::Point> path;
std::deque<ESM::Pathgrid::Point> path;
if(!isPointConnected(start, goal))
{
return path; // there is no path, return an empty path

@ -1,7 +1,7 @@
#ifndef GAME_MWMECHANICS_PATHGRID_H
#define GAME_MWMECHANICS_PATHGRID_H
#include <list>
#include <deque>
#include <components/esm/loadpgrd.hpp>
@ -38,8 +38,8 @@ namespace MWMechanics
// cells) coordinates
//
// NOTE: if start equals end an empty path is returned
std::list<ESM::Pathgrid::Point> aStarSearch(const int start,
const int end) const;
std::deque<ESM::Pathgrid::Point> aStarSearch(const int start, const int end) const;
private:
const ESM::Cell *mCell;

@ -3,7 +3,7 @@
#include <memory>
#include "../mwworld/ptr.hpp"
#include "ptrholder.hpp"
#include <osg/Vec3f>
#include <osg/Quat>
@ -22,30 +22,6 @@ namespace Resource
namespace MWPhysics
{
class PtrHolder
{
public:
virtual ~PtrHolder() {}
void updatePtr(const MWWorld::Ptr& updated)
{
mPtr = updated;
}
MWWorld::Ptr getPtr()
{
return mPtr;
}
MWWorld::ConstPtr getPtr() const
{
return mPtr;
}
protected:
MWWorld::Ptr mPtr;
};
class Actor : public PtrHolder
{
public:

@ -0,0 +1,54 @@
#include "heightfield.hpp"
#include <osg/Object>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <LinearMath/btTransform.h>
namespace MWPhysics
{
HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject)
{
mShape = new btHeightfieldTerrainShape(
sqrtVerts, sqrtVerts, heights, 1,
minH, maxH, 2,
PHY_FLOAT, false
);
mShape->setUseDiamondSubdivision(true);
mShape->setLocalScaling(btVector3(triSize, triSize, 1));
btTransform transform(btQuaternion::getIdentity(),
btVector3((x+0.5f) * triSize * (sqrtVerts-1),
(y+0.5f) * triSize * (sqrtVerts-1),
(maxH+minH)*0.5f));
mCollisionObject = new btCollisionObject;
mCollisionObject->setCollisionShape(mShape);
mCollisionObject->setWorldTransform(transform);
mHoldObject = holdObject;
}
HeightField::~HeightField()
{
delete mCollisionObject;
delete mShape;
}
btCollisionObject* HeightField::getCollisionObject()
{
return mCollisionObject;
}
const btCollisionObject* HeightField::getCollisionObject() const
{
return mCollisionObject;
}
const btHeightfieldTerrainShape* HeightField::getShape() const
{
return mShape;
}
}

@ -0,0 +1,36 @@
#ifndef OPENMW_MWPHYSICS_HEIGHTFIELD_H
#define OPENMW_MWPHYSICS_HEIGHTFIELD_H
#include <osg/ref_ptr>
class btCollisionObject;
class btHeightfieldTerrainShape;
namespace osg
{
class Object;
}
namespace MWPhysics
{
class HeightField
{
public:
HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject);
~HeightField();
btCollisionObject* getCollisionObject();
const btCollisionObject* getCollisionObject() const;
const btHeightfieldTerrainShape* getShape() const;
private:
btHeightfieldTerrainShape* mShape;
btCollisionObject* mCollisionObject;
osg::ref_ptr<const osg::Object> mHoldObject;
void operator=(const HeightField&);
HeightField(const HeightField&);
};
}
#endif

@ -0,0 +1,130 @@
#include "object.hpp"
#include "convert.hpp"
#include <components/debug/debuglog.hpp>
#include <components/nifosg/particle.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include <osg/Object>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <LinearMath/btTransform.h>
namespace MWPhysics
{
Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance)
: mShapeInstance(shapeInstance)
, mSolid(true)
{
mPtr = ptr;
mCollisionObject.reset(new btCollisionObject);
mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape());
mCollisionObject->setUserPointer(static_cast<PtrHolder*>(this));
setScale(ptr.getCellRef().getScale());
setRotation(toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
const float* pos = ptr.getRefData().getPosition().pos;
setOrigin(btVector3(pos[0], pos[1], pos[2]));
}
const Resource::BulletShapeInstance* Object::getShapeInstance() const
{
return mShapeInstance.get();
}
void Object::setScale(float scale)
{
mShapeInstance->setLocalScaling(btVector3(scale, scale, scale));
}
void Object::setRotation(const btQuaternion& quat)
{
mCollisionObject->getWorldTransform().setRotation(quat);
}
void Object::setOrigin(const btVector3& vec)
{
mCollisionObject->getWorldTransform().setOrigin(vec);
}
btCollisionObject* Object::getCollisionObject()
{
return mCollisionObject.get();
}
const btCollisionObject* Object::getCollisionObject() const
{
return mCollisionObject.get();
}
bool Object::isSolid() const
{
return mSolid;
}
void Object::setSolid(bool solid)
{
mSolid = solid;
}
bool Object::isAnimated() const
{
return !mShapeInstance->mAnimatedShapes.empty();
}
void Object::animateCollisionShapes(btCollisionWorld* collisionWorld)
{
if (mShapeInstance->mAnimatedShapes.empty())
return;
assert (mShapeInstance->getCollisionShape()->isCompound());
btCompoundShape* compound = static_cast<btCompoundShape*>(mShapeInstance->getCollisionShape());
for (std::map<int, int>::const_iterator it = mShapeInstance->mAnimatedShapes.begin(); it != mShapeInstance->mAnimatedShapes.end(); ++it)
{
int recIndex = it->first;
int shapeIndex = it->second;
std::map<int, osg::NodePath>::iterator nodePathFound = mRecIndexToNodePath.find(recIndex);
if (nodePathFound == mRecIndexToNodePath.end())
{
NifOsg::FindGroupByRecIndex visitor(recIndex);
mPtr.getRefData().getBaseNode()->accept(visitor);
if (!visitor.mFound)
{
Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " << mPtr.getCellRef().getRefId();
// Remove nonexistent nodes from animated shapes map and early out
mShapeInstance->mAnimatedShapes.erase(recIndex);
return;
}
osg::NodePath nodePath = visitor.mFoundPath;
nodePath.erase(nodePath.begin());
nodePathFound = mRecIndexToNodePath.insert(std::make_pair(recIndex, nodePath)).first;
}
osg::NodePath& nodePath = nodePathFound->second;
osg::Matrixf matrix = osg::computeLocalToWorld(nodePath);
matrix.orthoNormalize(matrix);
btTransform transform;
transform.setOrigin(toBullet(matrix.getTrans()) * compound->getLocalScaling());
for (int i=0; i<3; ++i)
for (int j=0; j<3; ++j)
transform.getBasis()[i][j] = matrix(j,i); // NB column/row major difference
// Note: we can not apply scaling here for now since we treat scaled shapes
// as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now
if (!(transform == compound->getChildTransform(shapeIndex)))
compound->updateChildTransform(shapeIndex, transform);
}
collisionWorld->updateSingleAabb(mCollisionObject.get());
}
}

@ -0,0 +1,48 @@
#ifndef OPENMW_MWPHYSICS_OBJECT_H
#define OPENMW_MWPHYSICS_OBJECT_H
#include "ptrholder.hpp"
#include <osg/Node>
#include <map>
#include <memory>
namespace Resource
{
class BulletShapeInstance;
}
class btCollisionObject;
class btCollisionWorld;
class btQuaternion;
class btVector3;
namespace MWPhysics
{
class Object : public PtrHolder
{
public:
Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance);
const Resource::BulletShapeInstance* getShapeInstance() const;
void setScale(float scale);
void setRotation(const btQuaternion& quat);
void setOrigin(const btVector3& vec);
btCollisionObject* getCollisionObject();
const btCollisionObject* getCollisionObject() const;
/// Return solid flag. Not used by the object itself, true by default.
bool isSolid() const;
void setSolid(bool solid);
bool isAnimated() const;
void animateCollisionShapes(btCollisionWorld* collisionWorld);
private:
std::unique_ptr<btCollisionObject> mCollisionObject;
osg::ref_ptr<Resource::BulletShapeInstance> mShapeInstance;
std::map<int, osg::NodePath> mRecIndexToNodePath;
bool mSolid;
};
}
#endif

@ -1,6 +1,11 @@
#include "physicssystem.hpp"
#include "physicssystem.hpp"
#include <stdexcept>
#include <unordered_map>
#include <fstream>
#include <array>
#include <boost/optional.hpp>
#include <osg/Group>
@ -16,6 +21,12 @@
#include <LinearMath/btQuickprof.h>
#include <DetourCommon.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
#include <DetourNavMeshQuery.h>
#include <Recast.h>
#include <components/nifbullet/bulletnifloader.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/bulletshapemanager.hpp>
@ -48,12 +59,12 @@
#include "actor.hpp"
#include "convert.hpp"
#include "trace.h"
#include "object.hpp"
#include "heightfield.hpp"
namespace MWPhysics
{
static const float sMaxSlope = 49.0f;
static const float sStepSizeUp = 34.0f;
static const float sStepSizeDown = 62.0f;
static const float sMinStep = 10.f;
static const float sGroundOffset = 1.0f;
@ -507,175 +518,6 @@ namespace MWPhysics
// ---------------------------------------------------------------
class HeightField
{
public:
HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject)
{
mShape = new btHeightfieldTerrainShape(
sqrtVerts, sqrtVerts, heights, 1,
minH, maxH, 2,
PHY_FLOAT, false
);
mShape->setUseDiamondSubdivision(true);
mShape->setLocalScaling(btVector3(triSize, triSize, 1));
btTransform transform(btQuaternion::getIdentity(),
btVector3((x+0.5f) * triSize * (sqrtVerts-1),
(y+0.5f) * triSize * (sqrtVerts-1),
(maxH+minH)*0.5f));
mCollisionObject = new btCollisionObject;
mCollisionObject->setCollisionShape(mShape);
mCollisionObject->setWorldTransform(transform);
mHoldObject = holdObject;
}
~HeightField()
{
delete mCollisionObject;
delete mShape;
}
btCollisionObject* getCollisionObject()
{
return mCollisionObject;
}
private:
btHeightfieldTerrainShape* mShape;
btCollisionObject* mCollisionObject;
osg::ref_ptr<const osg::Object> mHoldObject;
void operator=(const HeightField&);
HeightField(const HeightField&);
};
// --------------------------------------------------------------
class Object : public PtrHolder
{
public:
Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance)
: mShapeInstance(shapeInstance)
, mSolid(true)
{
mPtr = ptr;
mCollisionObject.reset(new btCollisionObject);
mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape());
mCollisionObject->setUserPointer(static_cast<PtrHolder*>(this));
setScale(ptr.getCellRef().getScale());
setRotation(toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
const float* pos = ptr.getRefData().getPosition().pos;
setOrigin(btVector3(pos[0], pos[1], pos[2]));
}
const Resource::BulletShapeInstance* getShapeInstance() const
{
return mShapeInstance.get();
}
void setScale(float scale)
{
mShapeInstance->getCollisionShape()->setLocalScaling(btVector3(scale,scale,scale));
}
void setRotation(const btQuaternion& quat)
{
mCollisionObject->getWorldTransform().setRotation(quat);
}
void setOrigin(const btVector3& vec)
{
mCollisionObject->getWorldTransform().setOrigin(vec);
}
btCollisionObject* getCollisionObject()
{
return mCollisionObject.get();
}
const btCollisionObject* getCollisionObject() const
{
return mCollisionObject.get();
}
/// Return solid flag. Not used by the object itself, true by default.
bool isSolid() const
{
return mSolid;
}
void setSolid(bool solid)
{
mSolid = solid;
}
bool isAnimated() const
{
return !mShapeInstance->mAnimatedShapes.empty();
}
void animateCollisionShapes(btCollisionWorld* collisionWorld)
{
if (mShapeInstance->mAnimatedShapes.empty())
return;
assert (mShapeInstance->getCollisionShape()->isCompound());
btCompoundShape* compound = static_cast<btCompoundShape*>(mShapeInstance->getCollisionShape());
for (std::map<int, int>::const_iterator it = mShapeInstance->mAnimatedShapes.begin(); it != mShapeInstance->mAnimatedShapes.end(); ++it)
{
int recIndex = it->first;
int shapeIndex = it->second;
std::map<int, osg::NodePath>::iterator nodePathFound = mRecIndexToNodePath.find(recIndex);
if (nodePathFound == mRecIndexToNodePath.end())
{
NifOsg::FindGroupByRecIndex visitor(recIndex);
mPtr.getRefData().getBaseNode()->accept(visitor);
if (!visitor.mFound)
{
Log(Debug::Warning) << "Warning: animateCollisionShapes can't find node " << recIndex << " for " << mPtr.getCellRef().getRefId();
// Remove nonexistent nodes from animated shapes map and early out
mShapeInstance->mAnimatedShapes.erase(recIndex);
return;
}
osg::NodePath nodePath = visitor.mFoundPath;
nodePath.erase(nodePath.begin());
nodePathFound = mRecIndexToNodePath.insert(std::make_pair(recIndex, nodePath)).first;
}
osg::NodePath& nodePath = nodePathFound->second;
osg::Matrixf matrix = osg::computeLocalToWorld(nodePath);
matrix.orthoNormalize(matrix);
btTransform transform;
transform.setOrigin(toBullet(matrix.getTrans()) * compound->getLocalScaling());
for (int i=0; i<3; ++i)
for (int j=0; j<3; ++j)
transform.getBasis()[i][j] = matrix(j,i); // NB column/row major difference
// Note: we can not apply scaling here for now since we treat scaled shapes
// as new shapes (btScaledBvhTriangleMeshShape) with 1.0 scale for now
if (!(transform == compound->getChildTransform(shapeIndex)))
compound->updateChildTransform(shapeIndex, transform);
}
collisionWorld->updateSingleAabb(mCollisionObject.get());
}
private:
std::unique_ptr<btCollisionObject> mCollisionObject;
osg::ref_ptr<Resource::BulletShapeInstance> mShapeInstance;
std::map<int, osg::NodePath> mRecIndexToNodePath;
bool mSolid;
};
// ---------------------------------------------------------------
PhysicsSystem::PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr<osg::Group> parentNode)
: mShapeManager(new Resource::BulletShapeManager(resourceSystem->getVFS(), resourceSystem->getSceneManager(), resourceSystem->getNifFileManager()))
@ -1171,6 +1013,14 @@ namespace MWPhysics
}
}
const HeightField* PhysicsSystem::getHeightField(int x, int y) const
{
const auto heightField = mHeightFields.find(std::make_pair(x, y));
if (heightField == mHeightFields.end())
return nullptr;
return heightField->second;
}
void PhysicsSystem::addObject (const MWWorld::Ptr& ptr, const std::string& mesh, int collisionType)
{
osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance = mShapeManager->getInstance(mesh);

@ -4,6 +4,7 @@
#include <memory>
#include <map>
#include <set>
#include <algorithm>
#include <osg/Quat>
#include <osg/ref_ptr>
@ -49,6 +50,9 @@ namespace MWPhysics
class Object;
class Actor;
static const float sMaxSlope = 49.0f;
static const float sStepSizeUp = 34.0f;
class PhysicsSystem
{
public:
@ -85,6 +89,8 @@ namespace MWPhysics
void removeHeightField (int x, int y);
const HeightField* getHeightField(int x, int y) const;
bool toggleCollisionMode();
bool isActorCollisionEnabled(const MWWorld::Ptr& ptr);
void setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled);
@ -172,6 +178,12 @@ namespace MWPhysics
bool isOnSolidGround (const MWWorld::Ptr& actor) const;
template <class Function>
void forEachAnimatedObject(Function&& function) const
{
std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function);
}
private:
void updateWater();

@ -0,0 +1,33 @@
#ifndef OPENMW_MWPHYSICS_PTRHOLDER_H
#define OPENMW_MWPHYSICS_PTRHOLDER_H
#include "../mwworld/ptr.hpp"
namespace MWPhysics
{
class PtrHolder
{
public:
virtual ~PtrHolder() {}
void updatePtr(const MWWorld::Ptr& updated)
{
mPtr = updated;
}
MWWorld::Ptr getPtr()
{
return mPtr;
}
MWWorld::ConstPtr getPtr() const
{
return mPtr;
}
protected:
MWWorld::Ptr mPtr;
};
}
#endif

@ -0,0 +1,99 @@
#include "actorspaths.hpp"
#include "vismask.hpp"
#include <components/sceneutil/agentpath.hpp>
#include <osg/PositionAttitudeTransform>
namespace MWRender
{
ActorsPaths::ActorsPaths(const osg::ref_ptr<osg::Group>& root, bool enabled)
: mRootNode(root)
, mEnabled(enabled)
{
}
ActorsPaths::~ActorsPaths()
{
if (mEnabled)
disable();
}
bool ActorsPaths::toggle()
{
if (mEnabled)
disable();
else
enable();
return mEnabled;
}
void ActorsPaths::update(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end,
const DetourNavigator::Settings& settings)
{
if (!mEnabled)
return;
const auto group = mGroups.find(actor);
if (group != mGroups.end())
mRootNode->removeChild(group->second);
const auto newGroup = SceneUtil::createAgentPathGroup(path, halfExtents, start, end, settings);
if (newGroup)
{
newGroup->setNodeMask(Mask_Debug);
mRootNode->addChild(newGroup);
mGroups[actor] = newGroup;
}
}
void ActorsPaths::remove(const MWWorld::ConstPtr& actor)
{
const auto group = mGroups.find(actor);
if (group != mGroups.end())
{
mRootNode->removeChild(group->second);
mGroups.erase(group);
}
}
void ActorsPaths::removeCell(const MWWorld::CellStore* const store)
{
for (auto it = mGroups.begin(); it != mGroups.end(); )
{
if (it->first.getCell() == store)
{
mRootNode->removeChild(it->second);
it = mGroups.erase(it);
}
else
++it;
}
}
void ActorsPaths::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated)
{
const auto it = mGroups.find(old);
if (it == mGroups.end())
return;
auto group = std::move(it->second);
mGroups.erase(it);
mGroups.insert(std::make_pair(updated, std::move(group)));
}
void ActorsPaths::enable()
{
std::for_each(mGroups.begin(), mGroups.end(),
[&] (const Groups::value_type& v) { mRootNode->addChild(v.second); });
mEnabled = true;
}
void ActorsPaths::disable()
{
std::for_each(mGroups.begin(), mGroups.end(),
[&] (const Groups::value_type& v) { mRootNode->removeChild(v.second); });
mEnabled = false;
}
}

@ -0,0 +1,51 @@
#ifndef OPENMW_MWRENDER_AGENTSPATHS_H
#define OPENMW_MWRENDER_AGENTSPATHS_H
#include <apps/openmw/mwworld/ptr.hpp>
#include <components/detournavigator/navigator.hpp>
#include <osg/ref_ptr>
#include <unordered_map>
#include <deque>
namespace osg
{
class Group;
}
namespace MWRender
{
class ActorsPaths
{
public:
ActorsPaths(const osg::ref_ptr<osg::Group>& root, bool enabled);
~ActorsPaths();
bool toggle();
void update(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end,
const DetourNavigator::Settings& settings);
void remove(const MWWorld::ConstPtr& actor);
void removeCell(const MWWorld::CellStore* const store);
void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated);
void enable();
void disable();
private:
using Groups = std::map<MWWorld::ConstPtr, osg::ref_ptr<osg::Group>>;
osg::ref_ptr<osg::Group> mRootNode;
Groups mGroups;
bool mEnabled;
};
}
#endif

@ -0,0 +1,71 @@
#include "navmesh.hpp"
#include "vismask.hpp"
#include <components/sceneutil/navmesh.hpp>
#include <osg/PositionAttitudeTransform>
namespace MWRender
{
NavMesh::NavMesh(const osg::ref_ptr<osg::Group>& root, bool enabled)
: mRootNode(root)
, mEnabled(enabled)
, mGeneration(0)
, mRevision(0)
{
}
NavMesh::~NavMesh()
{
if (mEnabled)
disable();
}
bool NavMesh::toggle()
{
if (mEnabled)
disable();
else
enable();
return mEnabled;
}
void NavMesh::update(const dtNavMesh& navMesh, const std::size_t id,
const std::size_t generation, const std::size_t revision, const DetourNavigator::Settings& settings)
{
if (!mEnabled || (mId == id && mGeneration >= generation && mRevision >= revision))
return;
mId = id;
mGeneration = generation;
mRevision = revision;
if (mGroup)
mRootNode->removeChild(mGroup);
mGroup = SceneUtil::createNavMeshGroup(navMesh, settings);
if (mGroup)
{
mGroup->setNodeMask(Mask_Debug);
mRootNode->addChild(mGroup);
}
}
void NavMesh::reset()
{
if (mGroup)
mRootNode->removeChild(mGroup);
}
void NavMesh::enable()
{
if (mGroup)
mRootNode->addChild(mGroup);
mEnabled = true;
}
void NavMesh::disable()
{
reset();
mEnabled = false;
}
}

@ -0,0 +1,43 @@
#ifndef OPENMW_MWRENDER_NAVMESH_H
#define OPENMW_MWRENDER_NAVMESH_H
#include <components/detournavigator/navigator.hpp>
#include <osg/ref_ptr>
namespace osg
{
class Group;
class Geometry;
}
namespace MWRender
{
class NavMesh
{
public:
NavMesh(const osg::ref_ptr<osg::Group>& root, bool enabled);
~NavMesh();
bool toggle();
void update(const dtNavMesh& navMesh, const std::size_t number, const std::size_t generation,
const std::size_t revision, const DetourNavigator::Settings& settings);
void reset();
void enable();
void disable();
private:
osg::ref_ptr<osg::Group> mRootNode;
bool mEnabled;
std::size_t mId = std::numeric_limits<std::size_t>::max();
std::size_t mGeneration;
std::size_t mRevision;
osg::ref_ptr<osg::Group> mGroup;
};
}
#endif

@ -46,6 +46,8 @@
#include <components/esm/loadcell.hpp>
#include <components/fallback/fallback.hpp>
#include <components/detournavigator/navigator.hpp>
#include <boost/algorithm/string.hpp>
#include "../mwworld/cellstore.hpp"
@ -63,6 +65,8 @@
#include "water.hpp"
#include "terrainstorage.hpp"
#include "util.hpp"
#include "navmesh.hpp"
#include "actorspaths.hpp"
namespace
{
@ -189,13 +193,16 @@ namespace MWRender
Resource::ResourceSystem* mResourceSystem;
};
RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath)
RenderingManager::RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath,
DetourNavigator::Navigator& navigator)
: mViewer(viewer)
, mRootNode(rootNode)
, mResourceSystem(resourceSystem)
, mWorkQueue(workQueue)
, mUnrefQueue(new SceneUtil::UnrefQueue)
, mNavigator(navigator)
, mLandFogStart(0.f)
, mLandFogEnd(std::numeric_limits<float>::max())
, mUnderwaterFogStart(0.f)
@ -228,6 +235,8 @@ namespace MWRender
mRootNode->addChild(mSceneRoot);
mNavMesh.reset(new NavMesh(mRootNode, Settings::Manager::getBool("enable nav mesh render", "Navigator")));
mActorsPaths.reset(new ActorsPaths(mRootNode, Settings::Manager::getBool("enable agents paths render", "Navigator")));
mPathgrid.reset(new Pathgrid(mRootNode));
mObjects.reset(new Objects(mResourceSystem, sceneRoot, mUnrefQueue.get()));
@ -465,6 +474,7 @@ namespace MWRender
void RenderingManager::removeCell(const MWWorld::CellStore *store)
{
mPathgrid->removeCell(store);
mActorsPaths->removeCell(store);
mObjects->removeCell(store);
if (store->getCell()->isExterior())
@ -482,7 +492,7 @@ namespace MWRender
{
mSky->setEnabled(enabled);
}
bool RenderingManager::toggleBorders()
{
mBorders = !mBorders;
@ -516,6 +526,14 @@ namespace MWRender
mViewer->getCamera()->setCullMask(mask);
return enabled;
}
else if (mode == Render_NavMesh)
{
return mNavMesh->toggle();
}
else if (mode == Render_ActorsPaths)
{
return mActorsPaths->toggle();
}
return false;
}
@ -581,6 +599,28 @@ namespace MWRender
mWater->update(dt);
}
const auto navMeshes = mNavigator.getNavMeshes();
auto it = navMeshes.begin();
for (std::size_t i = 0; it != navMeshes.end() && i < mNavMeshNumber; ++i)
++it;
if (it == navMeshes.end())
{
mNavMesh->reset();
}
else
{
try
{
const auto locked = it->second.lockConst();
mNavMesh->update(locked->getValue(), mNavMeshNumber, locked->getGeneration(),
locked->getNavMeshRevision(), mNavigator.getSettings());
}
catch (const std::exception& e)
{
Log(Debug::Error) << "NavMesh render update exception: " << e.what();
}
}
mCamera->update(dt, paused);
osg::Vec3f focal, cameraPos;
@ -642,6 +682,7 @@ namespace MWRender
void RenderingManager::removeObject(const MWWorld::Ptr &ptr)
{
mActorsPaths->remove(ptr);
mObjects->removeObject(ptr);
mWater->removeEmitter(ptr);
}
@ -749,8 +790,8 @@ namespace MWRender
osg::Vec3 directions[6] = {
rawCubemap ? osg::Vec3(1,0,0) : osg::Vec3(0,0,1),
osg::Vec3(0,0,-1),
osg::Vec3(-1,0,0),
osg::Vec3(0,0,-1),
osg::Vec3(-1,0,0),
rawCubemap ? osg::Vec3(0,0,1) : osg::Vec3(1,0,0),
osg::Vec3(0,1,0),
osg::Vec3(0,-1,0)};
@ -789,7 +830,7 @@ namespace MWRender
mFieldOfView = fovBackup;
if (rawCubemap) // for raw cubemap don't run on GPU, just merge the images
{
{
image->allocateImage(cubeSize * 6,cubeSize,images[0]->r(),images[0]->getPixelFormat(),images[0]->getDataType());
for (int i = 0; i < 6; ++i)
@ -797,7 +838,7 @@ namespace MWRender
return true;
}
// run on GPU now:
osg::ref_ptr<osg::TextureCubeMap> cubeTexture (new osg::TextureCubeMap);
@ -1025,6 +1066,7 @@ namespace MWRender
void RenderingManager::updatePtr(const MWWorld::Ptr &old, const MWWorld::Ptr &updated)
{
mObjects->updatePtr(old, updated);
mActorsPaths->updatePtr(old, updated);
}
void RenderingManager::spawnEffect(const std::string &model, const std::string &texture, const osg::Vec3f &worldPosition, float scale, bool isMagicVFX)
@ -1345,5 +1387,19 @@ namespace MWRender
return mTerrainStorage->getLandManager();
}
void RenderingManager::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const
{
mActorsPaths->update(actor, path, halfExtents, start, end, mNavigator.getSettings());
}
void RenderingManager::removeActorPath(const MWWorld::ConstPtr& actor) const
{
mActorsPaths->remove(actor);
}
void RenderingManager::setNavMeshNumber(const std::size_t value)
{
mNavMeshNumber = value;
}
}

@ -12,6 +12,8 @@
#include "renderinginterface.hpp"
#include "rendermode.hpp"
#include <deque>
namespace osg
{
class Group;
@ -55,6 +57,12 @@ namespace SceneUtil
class UnrefQueue;
}
namespace DetourNavigator
{
class Navigator;
struct Settings;
}
namespace MWRender
{
@ -68,12 +76,16 @@ namespace MWRender
class Water;
class TerrainStorage;
class LandManager;
class NavMesh;
class ActorsPaths;
class RenderingManager : public MWRender::RenderingInterface
{
public:
RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode, Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath);
RenderingManager(osgViewer::Viewer* viewer, osg::ref_ptr<osg::Group> rootNode,
Resource::ResourceSystem* resourceSystem, SceneUtil::WorkQueue* workQueue,
const Fallback::Map* fallback, const std::string& resourcePath,
DetourNavigator::Navigator& navigator);
~RenderingManager();
MWRender::Objects& getObjects();
@ -211,6 +223,13 @@ namespace MWRender
bool toggleBorders();
void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const;
void removeActorPath(const MWWorld::ConstPtr& actor) const;
void setNavMeshNumber(const std::size_t value);
private:
void updateProjectionMatrix();
void updateTextureFiltering();
@ -235,6 +254,10 @@ namespace MWRender
osg::ref_ptr<osg::Light> mSunLight;
DetourNavigator::Navigator& mNavigator;
std::unique_ptr<NavMesh> mNavMesh;
std::size_t mNavMeshNumber = 0;
std::unique_ptr<ActorsPaths> mActorsPaths;
std::unique_ptr<Pathgrid> mPathgrid;
std::unique_ptr<Objects> mObjects;
std::unique_ptr<Water> mWater;

@ -10,7 +10,9 @@ namespace MWRender
Render_Wireframe,
Render_Pathgrid,
Render_Water,
Render_Scene
Render_Scene,
Render_NavMesh,
Render_ActorsPaths,
};
}

@ -455,5 +455,8 @@ op 0x2000304: Show
op 0x2000305: Show, explicit
op 0x2000306: OnActivate, explicit
op 0x2000307: ToggleBorders, tb
op 0x2000308: ToggleNavMesh
op 0x2000309: ToggleActorsPaths
op 0x200030a: SetNavMeshNumber
opcodes 0x2000308-0x3ffffff unused
opcodes 0x200030b-0x3ffffff unused

@ -1317,6 +1317,53 @@ namespace MWScript
}
};
class OpToggleNavMesh : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
bool enabled =
MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_NavMesh);
runtime.getContext().report (enabled ?
"Navigation Mesh Rendering -> On" : "Navigation Mesh Rendering -> Off");
}
};
class OpToggleActorsPaths : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
bool enabled =
MWBase::Environment::get().getWorld()->toggleRenderMode (MWRender::Render_ActorsPaths);
runtime.getContext().report (enabled ?
"Agents Paths Rendering -> On" : "Agents Paths Rendering -> Off");
}
};
class OpSetNavMeshNumberToRender : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
const auto navMeshNumber = runtime[0].mInteger;
runtime.pop();
if (navMeshNumber < 0)
{
runtime.getContext().report("Invalid navmesh number: use not less than zero values");
return;
}
MWBase::Environment::get().getWorld()->setNavMeshNumberToRender(static_cast<std::size_t>(navMeshNumber));
}
};
void installOpcodes (Interpreter::Interpreter& interpreter)
{
interpreter.installSegment5 (Compiler::Misc::opcodeXBox, new OpXBox);
@ -1417,6 +1464,9 @@ namespace MWScript
interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph<ImplicitRef>);
interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph<ExplicitRef>);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleNavMesh, new OpToggleNavMesh);
interpreter.installSegment5 (Compiler::Misc::opcodeToggleActorsPaths, new OpToggleActorsPaths);
interpreter.installSegment5 (Compiler::Misc::opcodeSetNavMeshNumberToRender, new OpSetNavMeshNumberToRender);
}
}
}

@ -432,7 +432,7 @@ namespace MWWorld
mHasState = true;
}
int CellStore::count() const
std::size_t CellStore::count() const
{
return mMergedRefs.size();
}

@ -240,7 +240,7 @@ namespace MWWorld
ESM::FogState* getFog () const;
int count() const;
std::size_t count() const;
///< Return total number of references, including deleted ones.
void load ();
@ -283,7 +283,7 @@ namespace MWWorld
/// \attention This function also lists deleted (count 0) objects!
/// \return Iteration completed?
template<class Visitor>
bool forEachConst (Visitor& visitor) const
bool forEachConst (Visitor&& visitor) const
{
if (mState != State_Loaded)
return false;

@ -2,12 +2,19 @@
#include <limits>
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <components/debug/debuglog.hpp>
#include <components/loadinglistener/loadinglistener.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/settings/settings.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/debug.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -19,6 +26,10 @@
#include "../mwrender/landmanager.hpp"
#include "../mwphysics/physicssystem.hpp"
#include "../mwphysics/actor.hpp"
#include "../mwphysics/object.hpp"
#include "../mwphysics/heightfield.hpp"
#include "../mwphysics/convert.hpp"
#include "player.hpp"
#include "localscripts.hpp"
@ -30,25 +41,45 @@
namespace
{
osg::Quat makeActorOsgQuat(const ESM::Position& position)
{
return osg::Quat(position.rot[2], osg::Vec3(0, 0, -1));
}
osg::Quat makeInversedOrderObjectOsgQuat(const ESM::Position& position)
{
const float xr = position.rot[0];
const float yr = position.rot[1];
const float zr = position.rot[2];
return osg::Quat(xr, osg::Vec3(-1, 0, 0))
* osg::Quat(yr, osg::Vec3(0, -1, 0))
* osg::Quat(zr, osg::Vec3(0, 0, -1));
}
osg::Quat makeObjectOsgQuat(const ESM::Position& position)
{
const float xr = position.rot[0];
const float yr = position.rot[1];
const float zr = position.rot[2];
return osg::Quat(zr, osg::Vec3(0, 0, -1))
* osg::Quat(yr, osg::Vec3(0, -1, 0))
* osg::Quat(xr, osg::Vec3(-1, 0, 0));
}
void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, bool inverseRotationOrder)
void setNodeRotation(const MWWorld::Ptr& ptr, MWRender::RenderingManager& rendering, const bool inverseRotationOrder)
{
if (!ptr.getRefData().getBaseNode())
return;
osg::Quat worldRotQuat(ptr.getRefData().getPosition().rot[2], osg::Vec3(0,0,-1));
if (!ptr.getClass().isActor())
{
float xr = ptr.getRefData().getPosition().rot[0];
float yr = ptr.getRefData().getPosition().rot[1];
if (!inverseRotationOrder)
worldRotQuat = worldRotQuat * osg::Quat(yr, osg::Vec3(0,-1,0)) *
osg::Quat(xr, osg::Vec3(-1,0,0));
else
worldRotQuat = osg::Quat(xr, osg::Vec3(-1,0,0)) * osg::Quat(yr, osg::Vec3(0,-1,0)) * worldRotQuat;
}
rendering.rotateObject(ptr, worldRotQuat);
rendering.rotateObject(ptr,
ptr.getClass().isActor()
? makeActorOsgQuat(ptr.getRefData().getPosition())
: (inverseRotationOrder
? makeInversedOrderObjectOsgQuat(ptr.getRefData().getPosition())
: makeObjectOsgQuat(ptr.getRefData().getPosition()))
);
}
void addObject(const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics,
@ -84,6 +115,73 @@ namespace
MWBase::Environment::get().getWorld()->applyLoopingParticles(ptr);
}
void addObject(const MWWorld::Ptr& ptr, const MWPhysics::PhysicsSystem& physics, DetourNavigator::Navigator& navigator)
{
if (const auto object = physics.getObject(ptr))
{
if (ptr.getClass().isDoor() && !ptr.getCellRef().getTeleport())
{
const auto shape = object->getShapeInstance()->getCollisionShape();
btVector3 aabbMin;
btVector3 aabbMax;
shape->getAabb(btTransform::getIdentity(), aabbMin, aabbMax);
const auto center = (aabbMax + aabbMin) * 0.5f;
const auto distanceFromDoor = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 0.5f;
const auto toPoint = aabbMax.x() - aabbMin.x() < aabbMax.y() - aabbMin.y()
? btVector3(distanceFromDoor, 0, 0)
: btVector3(0, distanceFromDoor, 0);
const auto& transform = object->getCollisionObject()->getWorldTransform();
const btTransform closedDoorTransform(
MWPhysics::toBullet(makeObjectOsgQuat(ptr.getCellRef().getPosition())),
transform.getOrigin()
);
const auto start = DetourNavigator::makeOsgVec3f(closedDoorTransform(center + toPoint));
const auto startPoint = physics.castRay(start, start - osg::Vec3f(0, 0, 1000), ptr, {},
MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water);
const auto connectionStart = startPoint.mHit ? startPoint.mHitPos : start;
const auto end = DetourNavigator::makeOsgVec3f(closedDoorTransform(center - toPoint));
const auto endPoint = physics.castRay(end, end - osg::Vec3f(0, 0, 1000), ptr, {},
MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Water);
const auto connectionEnd = endPoint.mHit ? endPoint.mHitPos : end;
navigator.addObject(
DetourNavigator::ObjectId(object),
DetourNavigator::DoorShapes(
*shape,
object->getShapeInstance()->getAvoidCollisionShape(),
connectionStart,
connectionEnd
),
transform
);
}
else
{
navigator.addObject(
DetourNavigator::ObjectId(object),
DetourNavigator::ObjectShapes {
*object->getShapeInstance()->getCollisionShape(),
object->getShapeInstance()->getAvoidCollisionShape()
},
object->getCollisionObject()->getWorldTransform()
);
}
}
else if (const auto actor = physics.getActor(ptr))
{
const auto halfExtents = ptr.getCell()->isExterior()
? physics.getHalfExtents(MWBase::Environment::get().getWorld()->getPlayerPtr())
: actor->getHalfExtents();
navigator.addAgent(halfExtents);
}
}
void updateObjectRotation (const MWWorld::Ptr& ptr, MWPhysics::PhysicsSystem& physics,
MWRender::RenderingManager& rendering, bool inverseRotationOrder)
{
@ -110,24 +208,19 @@ namespace
MWWorld::CellStore& mCell;
bool mRescale;
Loading::Listener& mLoadingListener;
MWPhysics::PhysicsSystem& mPhysics;
MWRender::RenderingManager& mRendering;
std::vector<MWWorld::Ptr> mToInsert;
InsertVisitor (MWWorld::CellStore& cell, bool rescale, Loading::Listener& loadingListener,
MWPhysics::PhysicsSystem& physics, MWRender::RenderingManager& rendering);
InsertVisitor (MWWorld::CellStore& cell, bool rescale, Loading::Listener& loadingListener);
bool operator() (const MWWorld::Ptr& ptr);
void insert();
template <class AddObject>
void insert(AddObject&& addObject);
};
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, bool rescale,
Loading::Listener& loadingListener, MWPhysics::PhysicsSystem& physics,
MWRender::RenderingManager& rendering)
: mCell (cell), mRescale (rescale), mLoadingListener (loadingListener),
mPhysics (physics),
mRendering (rendering)
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, bool rescale, Loading::Listener& loadingListener)
: mCell (cell), mRescale (rescale), mLoadingListener (loadingListener)
{}
bool InsertVisitor::operator() (const MWWorld::Ptr& ptr)
@ -138,7 +231,8 @@ namespace
return true;
}
void InsertVisitor::insert()
template <class AddObject>
void InsertVisitor::insert(AddObject&& addObject)
{
for (std::vector<MWWorld::Ptr>::iterator it = mToInsert.begin(); it != mToInsert.end(); ++it)
{
@ -155,7 +249,7 @@ namespace
{
try
{
addObject(ptr, mPhysics, mRendering);
addObject(ptr);
}
catch (const std::exception& e)
{
@ -178,6 +272,11 @@ namespace
}
};
int getCellPositionDistanceToOrigin(const std::pair<int, int>& cellPosition)
{
return std::abs(cellPosition.first) + std::abs(cellPosition.second);
}
}
@ -233,15 +332,29 @@ namespace MWWorld
void Scene::unloadCell (CellStoreCollection::iterator iter)
{
Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
DetourNavigator::log("unload cell ", (*iter)->getCell()->getDescription());
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
ListAndResetObjectsVisitor visitor;
(*iter)->forEach<ListAndResetObjectsVisitor>(visitor);
for (std::vector<MWWorld::Ptr>::const_iterator iter2 (visitor.mObjects.begin());
iter2!=visitor.mObjects.end(); ++iter2)
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
const auto playerHalfExtents = mPhysics->getHalfExtents(player);
for (const auto& ptr : visitor.mObjects)
{
mPhysics->remove(*iter2);
if (const auto object = mPhysics->getObject(ptr))
navigator->removeObject(DetourNavigator::ObjectId(object));
else if (const auto actor = mPhysics->getActor(ptr))
{
navigator->removeAgent(ptr.getCell()->isExterior() ? playerHalfExtents : actor->getHalfExtents());
mRendering.removeActorPath(ptr);
}
mPhysics->remove(ptr);
}
const auto cellX = (*iter)->getCell()->getGridX();
const auto cellY = (*iter)->getCell()->getGridY();
if ((*iter)->getCell()->isExterior())
{
const ESM::Land* land =
@ -250,9 +363,18 @@ namespace MWWorld
(*iter)->getCell()->getGridY()
);
if (land && land->mDataTypes&ESM::Land::DATA_VHGT)
mPhysics->removeHeightField ((*iter)->getCell()->getGridX(), (*iter)->getCell()->getGridY());
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->removeObject(DetourNavigator::ObjectId(heightField));
mPhysics->removeHeightField(cellX, cellY);
}
}
if ((*iter)->getCell()->hasWater())
navigator->removeWater(osg::Vec2i(cellX, cellY));
navigator->update(player.getRefData().getPosition().asVec3());
MWBase::Environment::get().getMechanicsManager()->drop (*iter);
mRendering.removeCell(*iter);
@ -271,20 +393,24 @@ namespace MWWorld
if(result.second)
{
Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription();
DetourNavigator::log("load cell ", cell->getCell()->getDescription());
float verts = ESM::Land::LAND_SIZE;
float worldsize = ESM::Land::REAL_SIZE;
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const int cellX = cell->getCell()->getGridX();
const int cellY = cell->getCell()->getGridY();
// Load terrain physics first...
if (cell->getCell()->isExterior())
{
int cellX = cell->getCell()->getGridX();
int cellY = cell->getCell()->getGridY();
osg::ref_ptr<const ESMTerrain::LandObject> land = mRendering.getLandManager()->getLand(cellX, cellY);
const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0;
if (data)
{
mPhysics->addHeightField (data->mHeights, cellX, cell->getCell()->getGridY(), worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get());
mPhysics->addHeightField (data->mHeights, cellX, cellY, worldsize / (verts-1), verts, data->mMinHeight, data->mMaxHeight, land.get());
}
else
{
@ -292,6 +418,10 @@ namespace MWWorld
defaultHeight.resize(verts*verts, ESM::Land::DEFAULT_HEIGHT);
mPhysics->addHeightField (&defaultHeight[0], cell->getCell()->getGridX(), cell->getCell()->getGridY(), worldsize / (verts-1), verts, ESM::Land::DEFAULT_HEIGHT, ESM::Land::DEFAULT_HEIGHT, land.get());
}
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->addObject(DetourNavigator::ObjectId(heightField), *heightField->getShape(),
heightField->getCollisionObject()->getWorldTransform());
}
// register local scripts
@ -313,10 +443,25 @@ namespace MWWorld
{
mPhysics->enableWater(waterLevel);
mRendering.setWaterHeight(waterLevel);
if (cell->getCell()->isExterior())
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
}
else
{
navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
cell->getWaterLevel(), btTransform::getIdentity());
}
}
else
mPhysics->disableWater();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
mRendering.configureAmbient(cell->getCell());
}
@ -337,6 +482,10 @@ namespace MWWorld
void Scene::playerMoved(const osg::Vec3f &pos)
{
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
if (!mCurrentCell || !mCurrentCell->isExterior())
return;
@ -355,7 +504,7 @@ namespace MWWorld
}
}
void Scene::changeCellGrid (int X, int Y, bool changeEvent)
void Scene::changeCellGrid (int playerCellX, int playerCellY, bool changeEvent)
{
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
Loading::ScopedLoad load(loadingListener);
@ -369,8 +518,8 @@ namespace MWWorld
{
if ((*active)->getCell()->isExterior())
{
if (std::abs (X-(*active)->getCell()->getGridX())<=mHalfGridSize &&
std::abs (Y-(*active)->getCell()->getGridY())<=mHalfGridSize)
if (std::abs (playerCellX-(*active)->getCell()->getGridX())<=mHalfGridSize &&
std::abs (playerCellY-(*active)->getCell()->getGridY())<=mHalfGridSize)
{
// keep cells within the new grid
++active;
@ -380,11 +529,12 @@ namespace MWWorld
unloadCell (active++);
}
int refsToLoad = 0;
std::size_t refsToLoad = 0;
std::vector<std::pair<int, int>> cellsPositionsToLoad;
// get the number of refs to load
for (int x=X-mHalfGridSize; x<=X+mHalfGridSize; ++x)
for (int x = playerCellX - mHalfGridSize; x <= playerCellX + mHalfGridSize; ++x)
{
for (int y=Y-mHalfGridSize; y<=Y+mHalfGridSize; ++y)
for (int y = playerCellY - mHalfGridSize; y <= playerCellY + mHalfGridSize; ++y)
{
CellStoreCollection::iterator iter = mActiveCells.begin();
@ -400,40 +550,58 @@ namespace MWWorld
}
if (iter==mActiveCells.end())
{
refsToLoad += MWBase::Environment::get().getWorld()->getExterior(x, y)->count();
cellsPositionsToLoad.push_back(std::make_pair(x, y));
}
}
}
loadingListener->setProgressRange(refsToLoad);
const auto getDistanceToPlayerCell = [&] (const std::pair<int, int>& cellPosition)
{
return std::abs(cellPosition.first - playerCellX) + std::abs(cellPosition.second - playerCellY);
};
const auto getCellPositionPriority = [&] (const std::pair<int, int>& cellPosition)
{
return std::make_pair(getDistanceToPlayerCell(cellPosition), getCellPositionDistanceToOrigin(cellPosition));
};
std::sort(cellsPositionsToLoad.begin(), cellsPositionsToLoad.end(),
[&] (const std::pair<int, int>& lhs, const std::pair<int, int>& rhs) {
return getCellPositionPriority(lhs) < getCellPositionPriority(rhs);
});
// Load cells
for (int x=X-mHalfGridSize; x<=X+mHalfGridSize; ++x)
for (const auto& cellPosition : cellsPositionsToLoad)
{
for (int y=Y-mHalfGridSize; y<=Y+mHalfGridSize; ++y)
{
CellStoreCollection::iterator iter = mActiveCells.begin();
const auto x = cellPosition.first;
const auto y = cellPosition.second;
while (iter!=mActiveCells.end())
{
assert ((*iter)->getCell()->isExterior());
CellStoreCollection::iterator iter = mActiveCells.begin();
if (x==(*iter)->getCell()->getGridX() &&
y==(*iter)->getCell()->getGridY())
break;
while (iter != mActiveCells.end())
{
assert ((*iter)->getCell()->isExterior());
++iter;
}
if (x == (*iter)->getCell()->getGridX() &&
y == (*iter)->getCell()->getGridY())
break;
if (iter==mActiveCells.end())
{
CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y);
++iter;
}
loadCell (cell, loadingListener, changeEvent);
}
if (iter == mActiveCells.end())
{
CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(x, y);
loadCell (cell, loadingListener, changeEvent);
}
}
CellStore* current = MWBase::Environment::get().getWorld()->getExterior(X,Y);
CellStore* current = MWBase::Environment::get().getWorld()->getExterior(playerCellX, playerCellY);
MWBase::Environment::get().getWindowManager()->changeCell(current);
if (changeEvent)
@ -477,8 +645,9 @@ namespace MWWorld
mLastPlayerPos = pos.asVec3();
}
Scene::Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics)
: mCurrentCell (0), mCellChanged (false), mPhysics(physics), mRendering(rendering)
Scene::Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics,
DetourNavigator::Navigator& navigator)
: mCurrentCell (0), mCellChanged (false), mPhysics(physics), mRendering(rendering), mNavigator(navigator)
, mPreloadTimer(0.f)
, mHalfGridSize(Settings::Manager::getInt("exterior cell load distance", "Cells"))
, mCellLoadingThreshold(1024.f)
@ -555,8 +724,7 @@ namespace MWWorld
while (active!=mActiveCells.end())
unloadCell (active++);
int refsToLoad = cell->count();
loadingListener->setProgressRange(refsToLoad);
loadingListener->setProgressRange(cell->count());
// Load cell.
loadCell (cell, loadingListener, changeEvent);
@ -608,9 +776,10 @@ namespace MWWorld
void Scene::insertCell (CellStore &cell, bool rescale, Loading::Listener* loadingListener)
{
InsertVisitor insertVisitor (cell, rescale, *loadingListener, *mPhysics, mRendering);
InsertVisitor insertVisitor (cell, rescale, *loadingListener);
cell.forEach (insertVisitor);
insertVisitor.insert();
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering); });
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); });
// do adjustPosition (snapping actors to ground) after objects are loaded, so we don't depend on the loading order
AdjustPositionVisitor adjustPosVisitor;
@ -622,7 +791,11 @@ namespace MWWorld
try
{
addObject(ptr, *mPhysics, mRendering);
addObject(ptr, *mPhysics, mNavigator);
MWBase::Environment::get().getWorld()->scaleObject(ptr, ptr.getCellRef().getScale());
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
}
catch (std::exception& e)
{
@ -634,6 +807,20 @@ namespace MWWorld
{
MWBase::Environment::get().getMechanicsManager()->remove (ptr);
MWBase::Environment::get().getSoundManager()->stopSound3D (ptr);
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
if (const auto object = mPhysics->getObject(ptr))
{
navigator->removeObject(DetourNavigator::ObjectId(object));
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
}
else if (const auto actor = mPhysics->getActor(ptr))
{
const auto& halfExtents = ptr.getCell()->isExterior()
? mPhysics->getHalfExtents(MWBase::Environment::get().getWorld()->getPlayerPtr())
: actor->getHalfExtents();
navigator->removeAgent(halfExtents);
}
mPhysics->remove(ptr);
mRendering.removeObject (ptr);
if (ptr.getClass().isActor())

@ -6,6 +6,7 @@
#include <set>
#include <memory>
#include <unordered_map>
namespace osg
{
@ -27,6 +28,12 @@ namespace Loading
class Listener;
}
namespace DetourNavigator
{
class Navigator;
class Water;
}
namespace MWRender
{
class SkyManager;
@ -57,6 +64,7 @@ namespace MWWorld
bool mCellChanged;
MWPhysics::PhysicsSystem *mPhysics;
MWRender::RenderingManager& mRendering;
DetourNavigator::Navigator& mNavigator;
std::unique_ptr<CellPreloader> mPreloader;
float mPreloadTimer;
int mHalfGridSize;
@ -74,7 +82,7 @@ namespace MWWorld
void insertCell (CellStore &cell, bool rescale, Loading::Listener* loadingListener);
// Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center
void changeCellGrid (int X, int Y, bool changeEvent = true);
void changeCellGrid (int playerCellX, int playerCellY, bool changeEvent = true);
void getGridCenter(int& cellX, int& cellY);
@ -85,7 +93,8 @@ namespace MWWorld
public:
Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics);
Scene (MWRender::RenderingManager& rendering, MWPhysics::PhysicsSystem *physics,
DetourNavigator::Navigator& navigator);
~Scene();

@ -3,6 +3,9 @@
#include <osg/Group>
#include <osg/ComputeBoundsVisitor>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
@ -15,10 +18,15 @@
#include <components/files/collections.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/debug.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp"
@ -47,6 +55,7 @@
#include "../mwphysics/physicssystem.hpp"
#include "../mwphysics/actor.hpp"
#include "../mwphysics/collisiontype.hpp"
#include "../mwphysics/object.hpp"
#include "player.hpp"
#include "manualref.hpp"
@ -159,12 +168,6 @@ namespace MWWorld
mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0),
mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f)
{
mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode));
mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath));
mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get()));
mRendering->preloadCommonAssets();
mEsm.resize(contentFiles.size());
Loading::Listener* listener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
listener->loadingOn();
@ -193,9 +196,49 @@ namespace MWWorld
mSwimHeightScale = mStore.get<ESM::GameSetting>().find("fSwimHeightScale")->mValue.getFloat();
mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode));
DetourNavigator::Settings navigatorSettings;
navigatorSettings.mBorderSize = Settings::Manager::getInt("border size", "Navigator");
navigatorSettings.mCellHeight = Settings::Manager::getFloat("cell height", "Navigator");
navigatorSettings.mCellSize = Settings::Manager::getFloat("cell size", "Navigator");
navigatorSettings.mDetailSampleDist = Settings::Manager::getFloat("detail sample dist", "Navigator");
navigatorSettings.mDetailSampleMaxError = Settings::Manager::getFloat("detail sample max error", "Navigator");
navigatorSettings.mMaxClimb = MWPhysics::sStepSizeUp;
navigatorSettings.mMaxSimplificationError = Settings::Manager::getFloat("max simplification error", "Navigator");
navigatorSettings.mMaxSlope = MWPhysics::sMaxSlope;
navigatorSettings.mRecastScaleFactor = Settings::Manager::getFloat("recast scale factor", "Navigator");
navigatorSettings.mSwimHeightScale = mSwimHeightScale;
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.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");
navigatorSettings.mTileSize = Settings::Manager::getInt("tile size", "Navigator");
navigatorSettings.mAsyncNavMeshUpdaterThreads = static_cast<std::size_t>(Settings::Manager::getInt("async nav mesh updater threads", "Navigator"));
navigatorSettings.mMaxNavMeshTilesCacheSize = static_cast<std::size_t>(Settings::Manager::getInt("max nav mesh tiles cache size", "Navigator"));
navigatorSettings.mMaxPolygonPathSize = static_cast<std::size_t>(Settings::Manager::getInt("max polygon path size", "Navigator"));
navigatorSettings.mMaxSmoothPathSize = static_cast<std::size_t>(Settings::Manager::getInt("max smooth path size", "Navigator"));
navigatorSettings.mTrianglesPerChunk = static_cast<std::size_t>(Settings::Manager::getInt("triangles per chunk", "Navigator"));
navigatorSettings.mEnableWriteRecastMeshToFile = Settings::Manager::getBool("enable write recast mesh to file", "Navigator");
navigatorSettings.mEnableWriteNavMeshToFile = Settings::Manager::getBool("enable write nav mesh to file", "Navigator");
navigatorSettings.mRecastMeshPathPrefix = Settings::Manager::getString("recast mesh path prefix", "Navigator");
navigatorSettings.mNavMeshPathPrefix = Settings::Manager::getString("nav mesh path prefix", "Navigator");
navigatorSettings.mEnableRecastMeshFileNameRevision = Settings::Manager::getBool("enable recast mesh file name revision", "Navigator");
navigatorSettings.mEnableNavMeshFileNameRevision = Settings::Manager::getBool("enable nav mesh file name revision", "Navigator");
if (Settings::Manager::getBool("enable log", "Navigator"))
DetourNavigator::Log::instance().setSink(std::unique_ptr<DetourNavigator::FileSink>(
new DetourNavigator::FileSink(Settings::Manager::getString("log path", "Navigator"))));
mNavigator.reset(new DetourNavigator::Navigator(navigatorSettings));
mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath, *mNavigator));
mProjectileManager.reset(new ProjectileManager(mRendering->getLightRoot(), resourceSystem, mRendering.get(), mPhysics.get()));
mRendering->preloadCommonAssets();
mWeatherManager.reset(new MWWorld::WeatherManager(*mRendering, mFallback, mStore));
mWorldScene.reset(new Scene(*mRendering.get(), mPhysics.get()));
mWorldScene.reset(new Scene(*mRendering.get(), mPhysics.get(), *mNavigator));
}
void World::fillGlobalVariables()
@ -1506,6 +1549,32 @@ namespace MWWorld
moveObjectImp(player->first, player->second.x(), player->second.y(), player->second.z(), false);
}
void World::updateNavigator()
{
bool updated = false;
mPhysics->forEachAnimatedObject([&] (const MWPhysics::Object* object)
{
updated = updateNavigatorObject(object) || updated;
});
for (const auto& door : mDoorStates)
if (const auto object = mPhysics->getObject(door.first))
updated = updateNavigatorObject(object) || updated;
if (updated)
mNavigator->update(getPlayerPtr().getRefData().getPosition().asVec3());
}
bool World::updateNavigatorObject(const MWPhysics::Object* object)
{
const DetourNavigator::ObjectShapes shapes {
*object->getShapeInstance()->getCollisionShape(),
object->getShapeInstance()->getAvoidCollisionShape()
};
return mNavigator->updateObject(DetourNavigator::ObjectId(object), shapes, object->getCollisionObject()->getWorldTransform());
}
bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2)
{
int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door;
@ -1708,7 +1777,10 @@ namespace MWWorld
updateWeather(duration, paused);
if (!paused)
{
doPhysics (duration);
updateNavigator();
}
updatePlayer();
@ -2236,7 +2308,7 @@ namespace MWWorld
float waterlevel = cell->getWaterLevel();
// SwimHeightScale affects the upper z position an actor can swim to
// SwimHeightScale affects the upper z position an actor can swim to
// while in water. Based on observation from the original engine,
// the upper z position you get with a +1 SwimHeightScale is the depth
// limit for being able to cast water walking on an underwater target.
@ -2305,6 +2377,7 @@ namespace MWWorld
{
// Remove the old CharacterController
MWBase::Environment::get().getMechanicsManager()->remove(getPlayerPtr());
mNavigator->removeAgent(mPhysics->getHalfExtents(getPlayerPtr()));
mPhysics->remove(getPlayerPtr());
mRendering->removePlayer(getPlayerPtr());
@ -2339,6 +2412,8 @@ namespace MWWorld
mPhysics->addActor(getPlayerPtr(), model);
applyLoopingParticles(player);
mNavigator->addAgent(mPhysics->getHalfExtents(getPlayerPtr()));
}
World::RestPermitted World::canRest () const
@ -2379,7 +2454,7 @@ namespace MWWorld
{
mRendering->screenshot(image, w, h);
}
bool World::screenshot360(osg::Image* image, std::string settingStr)
{
return mRendering->screenshot360(image,settingStr);
@ -3552,7 +3627,7 @@ namespace MWWorld
continue;
}
else
mRendering->spawnEffect("meshes\\" + areaStatic->mModel, texture, origin, static_cast<float>(effectIt->mArea * 2));
mRendering->spawnEffect("meshes\\" + areaStatic->mModel, texture, origin, static_cast<float>(effectIt->mArea * 2));
// Play explosion sound (make sure to use NoTrack, since we will delete the projectile now)
static const std::string schools[] = {
@ -3704,4 +3779,20 @@ namespace MWWorld
}
}
DetourNavigator::Navigator* World::getNavigator() const
{
return mNavigator.get();
}
void World::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const
{
mRendering->updateActorPath(actor, path, halfExtents, start, end);
}
void World::setNavMeshNumberToRender(const std::size_t value)
{
mRendering->setNavMeshNumber(value);
}
}

@ -61,6 +61,11 @@ namespace ToUTF8
struct ContentLoader;
namespace MWPhysics
{
class Object;
}
namespace MWWorld
{
class WeatherManager;
@ -94,6 +99,7 @@ namespace MWWorld
std::unique_ptr<MWWorld::Player> mPlayer;
std::unique_ptr<MWPhysics::PhysicsSystem> mPhysics;
std::unique_ptr<DetourNavigator::Navigator> mNavigator;
std::unique_ptr<MWRender::RenderingManager> mRendering;
std::unique_ptr<MWWorld::Scene> mWorldScene;
std::unique_ptr<MWWorld::WeatherManager> mWeatherManager;
@ -147,6 +153,10 @@ namespace MWWorld
void doPhysics(float duration);
///< Run physics simulation and modify \a world accordingly.
void updateNavigator();
bool updateNavigatorObject(const MWPhysics::Object* object);
void ensureNeededRecords();
void fillGlobalVariables();
@ -693,6 +703,13 @@ namespace MWWorld
/// Preload VFX associated with this effect list
void preloadEffects(const ESM::EffectList* effectList) override;
DetourNavigator::Navigator* getNavigator() const override;
void updateActorPath(const MWWorld::ConstPtr& actor, const std::deque<osg::Vec3f>& path,
const osg::Vec3f& halfExtents, const osg::Vec3f& start, const osg::Vec3f& end) const override;
void setNavMeshNumberToRender(const std::size_t value) override;
};
}

@ -17,10 +17,21 @@ if (GTEST_FOUND AND GMOCK_FOUND)
misc/test_stringops.cpp
nifloader/testbulletnifloader.cpp
detournavigator/navigator.cpp
detournavigator/settingsutils.cpp
detournavigator/recastmeshbuilder.cpp
detournavigator/gettilespositions.cpp
detournavigator/recastmeshobject.cpp
detournavigator/navmeshtilescache.cpp
)
source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
find_package(RecastNavigation COMPONENTS DebugUtils Detour Recast REQUIRED)
include_directories(SYSTEM ${RecastNavigation_INCLUDE_DIRS})
openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
target_link_libraries(openmw_test_suite ${GMOCK_LIBRARIES} components)

@ -0,0 +1,104 @@
#include <components/detournavigator/gettilespositions.hpp>
#include <components/detournavigator/debug.hpp>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct CollectTilesPositions
{
std::vector<TilePosition>& mTilesPositions;
void operator ()(const TilePosition& value)
{
mTilesPositions.push_back(value);
}
};
struct DetourNavigatorGetTilesPositionsTest : Test
{
Settings mSettings;
std::vector<TilePosition> mTilesPositions;
CollectTilesPositions mCollect {mTilesPositions};
DetourNavigatorGetTilesPositionsTest()
{
mSettings.mBorderSize = 0;
mSettings.mCellSize = 0.5;
mSettings.mRecastScaleFactor = 1;
mSettings.mTileSize = 64;
}
};
TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_in_single_tile_should_return_one_tile)
{
getTilesPositions(osg::Vec3f(2, 2, 0), osg::Vec3f(31, 31, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_x_bounds_in_two_tiles_should_return_two_tiles)
{
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(32, 31, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(1, 0)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, for_object_with_y_bounds_in_two_tiles_should_return_two_tiles)
{
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31, 32, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0), TilePosition(0, 1)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_works_only_for_x_and_y_coordinates)
{
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31, 31, 32), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0)));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, tiling_should_work_with_negative_coordinates)
{
getTilesPositions(osg::Vec3f(-31, -31, 0), osg::Vec3f(31, 31, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(
TilePosition(-1, -1),
TilePosition(-1, 0),
TilePosition(0, -1),
TilePosition(0, 0)
));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, border_size_should_extend_tile_bounds)
{
mSettings.mBorderSize = 1;
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(31.5, 31.5, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(
TilePosition(-1, -1),
TilePosition(-1, 0),
TilePosition(-1, 1),
TilePosition(0, -1),
TilePosition(0, 0),
TilePosition(0, 1),
TilePosition(1, -1),
TilePosition(1, 0),
TilePosition(1, 1)
));
}
TEST_F(DetourNavigatorGetTilesPositionsTest, should_apply_recast_scale_factor)
{
mSettings.mRecastScaleFactor = 0.5;
getTilesPositions(osg::Vec3f(0, 0, 0), osg::Vec3f(32, 32, 1), mSettings, mCollect);
EXPECT_THAT(mTilesPositions, ElementsAre(TilePosition(0, 0)));
}
}

@ -0,0 +1,661 @@
#include "operators.hpp"
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <gtest/gtest.h>
#include <iterator>
#include <deque>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorNavigatorTest : Test
{
Settings mSettings;
std::unique_ptr<Navigator> mNavigator;
osg::Vec3f mPlayerPosition;
osg::Vec3f mAgentHalfExtents;
osg::Vec3f mStart;
osg::Vec3f mEnd;
std::deque<osg::Vec3f> mPath;
std::back_insert_iterator<std::deque<osg::Vec3f>> mOut;
DetourNavigatorNavigatorTest()
: mPlayerPosition(0, 0, 0)
, mAgentHalfExtents(29, 29, 66)
, mStart(-215, 215, 1)
, mEnd(215, -215, 1)
, mOut(mPath)
{
mSettings.mEnableWriteRecastMeshToFile = false;
mSettings.mEnableWriteNavMeshToFile = false;
mSettings.mEnableRecastMeshFileNameRevision = false;
mSettings.mEnableNavMeshFileNameRevision = false;
mSettings.mBorderSize = 16;
mSettings.mCellHeight = 0.2f;
mSettings.mCellSize = 0.2f;
mSettings.mDetailSampleDist = 6;
mSettings.mDetailSampleMaxError = 1;
mSettings.mMaxClimb = 34;
mSettings.mMaxSimplificationError = 1.3f;
mSettings.mMaxSlope = 49;
mSettings.mRecastScaleFactor = 0.017647058823529415f;
mSettings.mSwimHeightScale = 0.89999997615814208984375f;
mSettings.mMaxEdgeLen = 12;
mSettings.mMaxNavMeshQueryNodes = 2048;
mSettings.mMaxVertsPerPoly = 6;
mSettings.mRegionMergeSize = 20;
mSettings.mRegionMinSize = 8;
mSettings.mTileSize = 64;
mSettings.mAsyncNavMeshUpdaterThreads = 1;
mSettings.mMaxNavMeshTilesCacheSize = 1024 * 1024;
mSettings.mMaxPolygonPathSize = 1024;
mSettings.mMaxSmoothPathSize = 1024;
mSettings.mTrianglesPerChunk = 256;
mSettings.mMaxPolys = 4096;
mNavigator.reset(new Navigator(mSettings));
}
};
TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_throw_exception)
{
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), InvalidArgument);
}
TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception)
{
mNavigator->addAgent(mAgentHalfExtents);
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), NavigatorException);
}
TEST_F(DetourNavigatorNavigatorTest, find_path_for_removed_agent_should_throw_exception)
{
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->removeAgent(mAgentHalfExtents);
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), InvalidArgument);
}
TEST_F(DetourNavigatorNavigatorTest, add_agent_should_count_each_agent)
{
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->removeAgent(mAgentHalfExtents);
EXPECT_THROW(mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut), NavigatorException);
}
TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, add_object_should_change_navmesh)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
heightfieldShape.setLocalScaling(btVector3(128, 128, 1));
btBoxShape boxShape(btVector3(20, 20, 100));
btCompoundShape compoundShape;
compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, std::back_inserter(mPath));
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mPath.clear();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, std::back_inserter(mPath));
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.87827122211456298828125),
osg::Vec3f(-199.7968292236328125, 191.09100341796875, -3.54876613616943359375),
osg::Vec3f(-184.5936431884765625, 167.1819915771484375, -8.97847270965576171875),
osg::Vec3f(-169.3904571533203125, 143.2729949951171875, -14.40817737579345703125),
osg::Vec3f(-154.1872711181640625, 119.36397552490234375, -19.837890625),
osg::Vec3f(-138.9840850830078125, 95.45496368408203125, -25.2675952911376953125),
osg::Vec3f(-123.78090667724609375, 71.54595184326171875, -30.6972980499267578125),
osg::Vec3f(-108.57772064208984375, 47.636936187744140625, -36.12701416015625),
osg::Vec3f(-93.3745269775390625, 23.7279262542724609375, -40.754688262939453125),
osg::Vec3f(-78.17134857177734375, -0.18108306825160980224609375, -37.128787994384765625),
osg::Vec3f(-62.968158721923828125, -24.0900936126708984375, -33.50289154052734375),
osg::Vec3f(-47.764972686767578125, -47.999103546142578125, -30.797946929931640625),
osg::Vec3f(-23.852447509765625, -63.196765899658203125, -33.97112274169921875),
osg::Vec3f(0.0600789971649646759033203125, -78.39443206787109375, -37.14543914794921875),
osg::Vec3f(23.97260284423828125, -93.5920867919921875, -40.7740936279296875),
osg::Vec3f(47.885128021240234375, -108.78974151611328125, -36.051288604736328125),
osg::Vec3f(71.7976531982421875, -123.98740386962890625, -30.62355804443359375),
osg::Vec3f(95.71018218994140625, -139.18505859375, -25.1958160400390625),
osg::Vec3f(119.6226959228515625, -154.382720947265625, -19.7680912017822265625),
osg::Vec3f(143.53521728515625, -169.58038330078125, -14.3403491973876953125),
osg::Vec3f(167.4477386474609375, -184.778045654296875, -8.91261768341064453125),
osg::Vec3f(191.360260009765625, -199.9757080078125, -3.484879016876220703125),
osg::Vec3f(215, -215, 1.87827455997467041015625),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape heightfieldShape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
heightfieldShape.setLocalScaling(btVector3(128, 128, 1));
btBoxShape boxShape(btVector3(20, 20, 100));
btCompoundShape compoundShape;
compoundShape.addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(0, 0, 0)), &boxShape);
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&heightfieldShape), heightfieldShape, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, std::back_inserter(mPath));
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.87827122211456298828125),
osg::Vec3f(-199.7968292236328125, 191.09100341796875, -3.54876613616943359375),
osg::Vec3f(-184.5936431884765625, 167.1819915771484375, -8.97847270965576171875),
osg::Vec3f(-169.3904571533203125, 143.2729949951171875, -14.40817737579345703125),
osg::Vec3f(-154.1872711181640625, 119.36397552490234375, -19.837890625),
osg::Vec3f(-138.9840850830078125, 95.45496368408203125, -25.2675952911376953125),
osg::Vec3f(-123.78090667724609375, 71.54595184326171875, -30.6972980499267578125),
osg::Vec3f(-108.57772064208984375, 47.636936187744140625, -36.12701416015625),
osg::Vec3f(-93.3745269775390625, 23.7279262542724609375, -40.754688262939453125),
osg::Vec3f(-78.17134857177734375, -0.18108306825160980224609375, -37.128787994384765625),
osg::Vec3f(-62.968158721923828125, -24.0900936126708984375, -33.50289154052734375),
osg::Vec3f(-47.764972686767578125, -47.999103546142578125, -30.797946929931640625),
osg::Vec3f(-23.852447509765625, -63.196765899658203125, -33.97112274169921875),
osg::Vec3f(0.0600789971649646759033203125, -78.39443206787109375, -37.14543914794921875),
osg::Vec3f(23.97260284423828125, -93.5920867919921875, -40.7740936279296875),
osg::Vec3f(47.885128021240234375, -108.78974151611328125, -36.051288604736328125),
osg::Vec3f(71.7976531982421875, -123.98740386962890625, -30.62355804443359375),
osg::Vec3f(95.71018218994140625, -139.18505859375, -25.1958160400390625),
osg::Vec3f(119.6226959228515625, -154.382720947265625, -19.7680912017822265625),
osg::Vec3f(143.53521728515625, -169.58038330078125, -14.3403491973876953125),
osg::Vec3f(167.4477386474609375, -184.778045654296875, -8.91261768341064453125),
osg::Vec3f(191.360260009765625, -199.9757080078125, -3.484879016876220703125),
osg::Vec3f(215, -215, 1.87827455997467041015625),
})) << mPath;
compoundShape.updateChildTransform(0, btTransform(btMatrix3x3::getIdentity(), btVector3(1000, 0, 0)));
mNavigator->updateObject(ObjectId(&compoundShape), compoundShape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mPath.clear();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_should_use_higher)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
const std::array<btScalar, 5 * 5> heightfieldData2 {{
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
}};
btHeightfieldTerrainShape shape2(5, 5, heightfieldData2.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape2.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape2), shape2, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.96328866481781005859375),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -0.2422157227993011474609375),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -2.44772052764892578125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -4.653223514556884765625),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -6.858728885650634765625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -9.0642337799072265625),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -11.26973724365234375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -13.26497173309326171875),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -15.24860286712646484375),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -17.2322368621826171875),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -19.2158660888671875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -20.1338443756103515625),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -18.150211334228515625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -16.1665802001953125),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -14.18294811248779296875),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -12.19931507110595703125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -10.08488559722900390625),
osg::Vec3f(125.5897216796875, -125.5897216796875, -7.879383563995361328125),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -5.673877239227294921875),
osg::Vec3f(165.659088134765625, -165.659088134765625, -3.4683735370635986328125),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -1.2628715038299560546875),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, 0.9426348209381103515625),
osg::Vec3f(215, -215, 1.96328866481781005859375),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_around_avoid_shape)
{
std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
std::array<btScalar, 5 * 5> heightfieldDataAvoid {{
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
-25, -25, -25, -25, -25,
}};
btHeightfieldTerrainShape shapeAvoid(5, 5, heightfieldDataAvoid.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shapeAvoid.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), ObjectShapes {shape, &shapeAvoid}, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.9393787384033203125),
osg::Vec3f(-200.8159637451171875, 190.47265625, -0.639537751674652099609375),
osg::Vec3f(-186.6319427490234375, 165.9453125, -3.2184507846832275390625),
osg::Vec3f(-172.447906494140625, 141.41796875, -5.797363758087158203125),
osg::Vec3f(-158.263885498046875, 116.8906097412109375, -8.37627887725830078125),
osg::Vec3f(-144.079864501953125, 92.3632659912109375, -10.95519161224365234375),
osg::Vec3f(-129.89581298828125, 67.83591461181640625, -13.534107208251953125),
osg::Vec3f(-115.7117919921875, 43.308563232421875, -16.1130199432373046875),
osg::Vec3f(-101.5277557373046875, 18.7812137603759765625, -18.6919345855712890625),
osg::Vec3f(-87.34372711181640625, -5.7461376190185546875, -20.4680538177490234375),
osg::Vec3f(-67.02922821044921875, -25.4970550537109375, -20.514247894287109375),
osg::Vec3f(-46.714717864990234375, -45.2479705810546875, -20.5604457855224609375),
osg::Vec3f(-26.40021514892578125, -64.99889373779296875, -20.6066417694091796875),
osg::Vec3f(-6.085712432861328125, -84.74980926513671875, -20.652835845947265625),
osg::Vec3f(14.22879505157470703125, -104.50072479248046875, -18.151393890380859375),
osg::Vec3f(39.05098724365234375, -118.16222381591796875, -15.6674861907958984375),
osg::Vec3f(63.87317657470703125, -131.82373046875, -13.18357944488525390625),
osg::Vec3f(88.69537353515625, -145.4852142333984375, -10.69967365264892578125),
osg::Vec3f(113.51757049560546875, -159.146697998046875, -8.21576690673828125),
osg::Vec3f(138.3397674560546875, -172.808197021484375, -5.731858730316162109375),
osg::Vec3f(163.1619720458984375, -186.469696044921875, -3.2479503154754638671875),
osg::Vec3f(187.984161376953125, -200.1311798095703125, -0.764044582843780517578125),
osg::Vec3f(212.8063507080078125, -213.7926788330078125, 1.7198636531829833984375),
osg::Vec3f(215, -215, 1.93937528133392333984375),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_ground_lower_than_water_with_only_swim_flag)
{
std::array<btScalar, 5 * 5> heightfieldData {{
-50, -50, -50, -50, 0,
-50, -100, -150, -100, -50,
-50, -150, -200, -150, -100,
-50, -100, -150, -100, -100,
0, -50, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, 300, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mStart.z() = 300;
mEnd.x() = 0;
mEnd.z() = 300;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_swim, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, 185.33331298828125),
osg::Vec3f(0, 186.6666717529296875, 185.33331298828125),
osg::Vec3f(0, 158.333343505859375, 185.33331298828125),
osg::Vec3f(0, 130.0000152587890625, 185.33331298828125),
osg::Vec3f(0, 101.66667938232421875, 185.33331298828125),
osg::Vec3f(0, 73.333343505859375, 185.33331298828125),
osg::Vec3f(0, 45.0000152587890625, 185.33331298828125),
osg::Vec3f(0, 16.6666812896728515625, 185.33331298828125),
osg::Vec3f(0, -11.66664981842041015625, 185.33331298828125),
osg::Vec3f(0, -39.999980926513671875, 185.33331298828125),
osg::Vec3f(0, -68.33331298828125, 185.33331298828125),
osg::Vec3f(0, -96.66664886474609375, 185.33331298828125),
osg::Vec3f(0, -124.99997711181640625, 185.33331298828125),
osg::Vec3f(0, -153.33331298828125, 185.33331298828125),
osg::Vec3f(0, -181.6666412353515625, 185.33331298828125),
osg::Vec3f(0, -209.999969482421875, 185.33331298828125),
osg::Vec3f(0, -215, 185.33331298828125),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_swim_and_walk_flags)
{
std::array<btScalar, 7 * 7> heightfieldData {{
0, 0, 0, 0, 0, 0, 0,
0, -100, -100, -100, -100, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -150, -200, -150, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -100, -100, -100, -100, 0,
0, 0, 0, 0, 0, 0, 0,
}};
btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mEnd.x() = 0;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_swim | Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, -94.75363922119140625),
osg::Vec3f(0, 186.6666717529296875, -106.0000152587890625),
osg::Vec3f(0, 158.333343505859375, -115.85507965087890625),
osg::Vec3f(0, 130.0000152587890625, -125.71016693115234375),
osg::Vec3f(0, 101.66667938232421875, -135.5652313232421875),
osg::Vec3f(0, 73.333343505859375, -143.3333587646484375),
osg::Vec3f(0, 45.0000152587890625, -143.3333587646484375),
osg::Vec3f(0, 16.6666812896728515625, -143.3333587646484375),
osg::Vec3f(0, -11.66664981842041015625, -143.3333587646484375),
osg::Vec3f(0, -39.999980926513671875, -143.3333587646484375),
osg::Vec3f(0, -68.33331298828125, -143.3333587646484375),
osg::Vec3f(0, -96.66664886474609375, -137.3043670654296875),
osg::Vec3f(0, -124.99997711181640625, -127.44930267333984375),
osg::Vec3f(0, -153.33331298828125, -117.5942230224609375),
osg::Vec3f(0, -181.6666412353515625, -107.7391510009765625),
osg::Vec3f(0, -209.999969482421875, -97.79712677001953125),
osg::Vec3f(0, -215, -94.753631591796875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_water_when_ground_cross_water_with_max_int_cells_size_and_swim_and_walk_flags)
{
std::array<btScalar, 7 * 7> heightfieldData {{
0, 0, 0, 0, 0, 0, 0,
0, -100, -100, -100, -100, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -150, -200, -150, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -100, -100, -100, -100, 0,
0, 0, 0, 0, 0, 0, 0,
}};
btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->addWater(osg::Vec2i(0, 0), std::numeric_limits<int>::max(), -25, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mEnd.x() = 0;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_swim | Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, -94.75363922119140625),
osg::Vec3f(0, 186.6666717529296875, -106.0000152587890625),
osg::Vec3f(0, 158.333343505859375, -115.85507965087890625),
osg::Vec3f(0, 130.0000152587890625, -125.71016693115234375),
osg::Vec3f(0, 101.66667938232421875, -135.5652313232421875),
osg::Vec3f(0, 73.333343505859375, -143.3333587646484375),
osg::Vec3f(0, 45.0000152587890625, -143.3333587646484375),
osg::Vec3f(0, 16.6666812896728515625, -143.3333587646484375),
osg::Vec3f(0, -11.66664981842041015625, -143.3333587646484375),
osg::Vec3f(0, -39.999980926513671875, -143.3333587646484375),
osg::Vec3f(0, -68.33331298828125, -143.3333587646484375),
osg::Vec3f(0, -96.66664886474609375, -137.3043670654296875),
osg::Vec3f(0, -124.99997711181640625, -127.44930267333984375),
osg::Vec3f(0, -153.33331298828125, -117.5942230224609375),
osg::Vec3f(0, -181.6666412353515625, -107.7391510009765625),
osg::Vec3f(0, -209.999969482421875, -97.79712677001953125),
osg::Vec3f(0, -215, -94.753631591796875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, path_should_be_over_ground_when_ground_cross_water_with_only_walk_flag)
{
std::array<btScalar, 7 * 7> heightfieldData {{
0, 0, 0, 0, 0, 0, 0,
0, -100, -100, -100, -100, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -150, -200, -150, -100, 0,
0, -100, -150, -150, -150, -100, 0,
0, -100, -100, -100, -100, -100, 0,
0, 0, 0, 0, 0, 0, 0,
}};
btHeightfieldTerrainShape shape(7, 7, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addWater(osg::Vec2i(0, 0), 128 * 4, -25, btTransform::getIdentity());
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mStart.x() = 0;
mEnd.x() = 0;
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(0, 215, -94.75363922119140625),
osg::Vec3f(9.8083515167236328125, 188.4185333251953125, -105.19994354248046875),
osg::Vec3f(19.6167049407958984375, 161.837066650390625, -114.25496673583984375),
osg::Vec3f(29.42505645751953125, 135.255615234375, -123.309967041015625),
osg::Vec3f(39.23340606689453125, 108.674163818359375, -132.3649749755859375),
osg::Vec3f(49.04175567626953125, 82.09270477294921875, -137.2874755859375),
osg::Vec3f(58.8501129150390625, 55.5112457275390625, -139.2451171875),
osg::Vec3f(68.6584625244140625, 28.9297885894775390625, -141.2027740478515625),
osg::Vec3f(78.4668121337890625, 2.3483295440673828125, -143.1604156494140625),
osg::Vec3f(88.27516937255859375, -24.233127593994140625, -141.3894805908203125),
osg::Vec3f(83.73651885986328125, -52.2005767822265625, -142.3761444091796875),
osg::Vec3f(79.19786834716796875, -80.16802978515625, -143.114837646484375),
osg::Vec3f(64.8477935791015625, -104.598602294921875, -137.840911865234375),
osg::Vec3f(50.497714996337890625, -129.0291748046875, -131.45831298828125),
osg::Vec3f(36.147632598876953125, -153.459747314453125, -121.42321014404296875),
osg::Vec3f(21.7975559234619140625, -177.8903350830078125, -111.38809967041015625),
osg::Vec3f(7.44747829437255859375, -202.3209075927734375, -101.1938323974609375),
osg::Vec3f(0, -215, -94.753631591796875),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_remove_and_update_then_find_path_should_return_path)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->removeObject(ObjectId(&shape));
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
mNavigator->findPath(mAgentHalfExtents, mStart, mEnd, Flag_walk, mOut);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>({
osg::Vec3f(-215, 215, 1.85963428020477294921875),
osg::Vec3f(-194.9653167724609375, 194.9653167724609375, -6.5760211944580078125),
osg::Vec3f(-174.930633544921875, 174.930633544921875, -15.01167774200439453125),
osg::Vec3f(-154.8959503173828125, 154.8959503173828125, -23.4473323822021484375),
osg::Vec3f(-134.86126708984375, 134.86126708984375, -31.8829898834228515625),
osg::Vec3f(-114.82657623291015625, 114.82657623291015625, -40.3186492919921875),
osg::Vec3f(-94.7918853759765625, 94.7918853759765625, -47.39907073974609375),
osg::Vec3f(-74.75719451904296875, 74.75719451904296875, -53.7258148193359375),
osg::Vec3f(-54.722499847412109375, 54.722499847412109375, -60.052555084228515625),
osg::Vec3f(-34.68780517578125, 34.68780517578125, -66.37929534912109375),
osg::Vec3f(-14.6531162261962890625, 14.6531162261962890625, -72.70604705810546875),
osg::Vec3f(5.3815765380859375, -5.3815765380859375, -75.35065460205078125),
osg::Vec3f(25.41626739501953125, -25.41626739501953125, -67.96945953369140625),
osg::Vec3f(45.450958251953125, -45.450958251953125, -60.58824920654296875),
osg::Vec3f(65.48564910888671875, -65.48564910888671875, -53.20705413818359375),
osg::Vec3f(85.5203399658203125, -85.5203399658203125, -45.825855255126953125),
osg::Vec3f(105.55503082275390625, -105.55503082275390625, -38.44464874267578125),
osg::Vec3f(125.5897216796875, -125.5897216796875, -31.063449859619140625),
osg::Vec3f(145.6244049072265625, -145.6244049072265625, -23.6822509765625),
osg::Vec3f(165.659088134765625, -165.659088134765625, -16.3010540008544921875),
osg::Vec3f(185.6937713623046875, -185.6937713623046875, -8.91985416412353515625),
osg::Vec3f(205.7284698486328125, -205.7284698486328125, -1.53864824771881103515625),
osg::Vec3f(215, -215, 1.877177715301513671875),
})) << mPath;
}
}

@ -0,0 +1,312 @@
#include "operators.hpp"
#include <components/detournavigator/navmeshtilescache.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <components/detournavigator/recastmesh.hpp>
#include <LinearMath/btTransform.h>
#include <gtest/gtest.h>
namespace DetourNavigator
{
static inline bool operator ==(const NavMeshDataRef& lhs, const NavMeshDataRef& rhs)
{
return std::make_pair(lhs.mValue, lhs.mSize) == std::make_pair(rhs.mValue, rhs.mSize);
}
}
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorNavMeshTilesCacheTest : Test
{
const osg::Vec3f mAgentHalfExtents {1, 2, 3};
const TilePosition mTilePosition {0, 0};
const std::vector<int> mIndices {{0, 1, 2}};
const std::vector<float> mVertices {{0, 0, 0, 1, 0, 0, 1, 1, 0}};
const std::vector<AreaType> mAreaTypes {1, AreaType_ground};
const std::vector<RecastMesh::Water> mWater {};
const std::size_t mTrianglesPerChunk {1};
const RecastMesh mRecastMesh {mIndices, mVertices, mAreaTypes, mWater, mTrianglesPerChunk};
const std::vector<OffMeshConnection> mOffMeshConnections {};
unsigned char* const mData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData mNavMeshData {mData, 1};
};
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_empty_cache_should_return_empty_value)
{
const std::size_t maxSize = 0;
NavMeshTilesCache cache(maxSize);
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_for_not_enought_cache_size_should_return_empty_value)
{
const std::size_t maxSize = 0;
NavMeshTilesCache cache(maxSize);
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData)));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_return_cached_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_existing_element_should_throw_exception)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_THROW(
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(anotherNavMeshData)),
InvalidArgument
);
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_should_return_cached_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
const auto result = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(result);
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_agent_half_extents_should_return_empty_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const osg::Vec3f unexsistentAgentHalfExtents {1, 1, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.get(unexsistentAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_tile_position_should_return_empty_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const TilePosition unexistentTilePosition {1, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.get(mAgentHalfExtents, unexistentTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, get_for_cache_miss_by_recast_mesh_should_return_empty_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh unexistentRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, unexistentRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
const auto result = cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData));
ASSERT_TRUE(result);
EXPECT_EQ(result.get(), (NavMeshDataRef {anotherData, 1}));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_used_value)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_set_value)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> leastRecentlySetWater {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh leastRecentlySetRecastMesh {mIndices, mVertices, mAreaTypes, leastRecentlySetWater,
mTrianglesPerChunk};
const auto leastRecentlySetData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData leastRecentlySetNavMeshData {leastRecentlySetData, 1};
const std::vector<RecastMesh::Water> mostRecentlySetWater {1, RecastMesh::Water {2, btTransform::getIdentity()}};
const RecastMesh mostRecentlySetRecastMesh {mIndices, mVertices, mAreaTypes, mostRecentlySetWater,
mTrianglesPerChunk};
const auto mostRecentlySetData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData mostRecentlySetNavMeshData {mostRecentlySetData, 1};
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections,
std::move(leastRecentlySetNavMeshData)));
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections,
std::move(mostRecentlySetNavMeshData)));
const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlySetRecastMesh, mOffMeshConnections));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlySetRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_replace_unused_least_recently_used_value)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> leastRecentlyUsedWater {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh leastRecentlyUsedRecastMesh {mIndices, mVertices, mAreaTypes, leastRecentlyUsedWater,
mTrianglesPerChunk};
const auto leastRecentlyUsedData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData leastRecentlyUsedNavMeshData {leastRecentlyUsedData, 1};
const std::vector<RecastMesh::Water> mostRecentlyUsedWater {1, RecastMesh::Water {2, btTransform::getIdentity()}};
const RecastMesh mostRecentlyUsedRecastMesh {mIndices, mVertices, mAreaTypes, mostRecentlyUsedWater,
mTrianglesPerChunk};
const auto mostRecentlyUsedData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData mostRecentlyUsedNavMeshData {mostRecentlyUsedData, 1};
cache.set(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections,
std::move(leastRecentlyUsedNavMeshData));
cache.set(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections,
std::move(mostRecentlyUsedNavMeshData));
{
const auto value = cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections);
ASSERT_TRUE(value);
ASSERT_EQ(value.get(), (NavMeshDataRef {leastRecentlyUsedData, 1}));
}
{
const auto value = cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections);
ASSERT_TRUE(value);
ASSERT_EQ(value.get(), (NavMeshDataRef {mostRecentlyUsedData, 1}));
}
const auto result = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
EXPECT_EQ(result.get(), (NavMeshDataRef {mData, 1}));
EXPECT_FALSE(cache.get(mAgentHalfExtents, mTilePosition, leastRecentlyUsedRecastMesh, mOffMeshConnections));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mostRecentlyUsedRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_cache_max_size)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh tooLargeRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto tooLargeData = reinterpret_cast<unsigned char*>(dtAlloc(2, DT_ALLOC_PERM));
NavMeshData tooLargeNavMeshData {tooLargeData, 2};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections,
std::move(tooLargeNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, set_should_not_replace_unused_least_recently_used_value_when_item_does_not_not_fit_size_of_unused_items)
{
const std::size_t maxSize = 2;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> anotherWater {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, anotherWater, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
const std::vector<RecastMesh::Water> tooLargeWater {1, RecastMesh::Water {2, btTransform::getIdentity()}};
const RecastMesh tooLargeRecastMesh {mIndices, mVertices, mAreaTypes, tooLargeWater, mTrianglesPerChunk};
const auto tooLargeData = reinterpret_cast<unsigned char*>(dtAlloc(2, DT_ALLOC_PERM));
NavMeshData tooLargeNavMeshData {tooLargeData, 2};
const auto value = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections,
std::move(mNavMeshData));
ASSERT_TRUE(value);
ASSERT_TRUE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, tooLargeRecastMesh, mOffMeshConnections,
std::move(tooLargeNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_used_after_set_then_used_by_get_item_should_left_this_item_available)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
const auto firstCopy = cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
ASSERT_TRUE(firstCopy);
{
const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(secondCopy);
}
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
TEST_F(DetourNavigatorNavMeshTilesCacheTest, release_twice_used_item_should_left_this_item_available)
{
const std::size_t maxSize = 1;
NavMeshTilesCache cache(maxSize);
const std::vector<RecastMesh::Water> water {1, RecastMesh::Water {1, btTransform::getIdentity()}};
const RecastMesh anotherRecastMesh {mIndices, mVertices, mAreaTypes, water, mTrianglesPerChunk};
const auto anotherData = reinterpret_cast<unsigned char*>(dtAlloc(1, DT_ALLOC_PERM));
NavMeshData anotherNavMeshData {anotherData, 1};
cache.set(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections, std::move(mNavMeshData));
const auto firstCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(firstCopy);
{
const auto secondCopy = cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections);
ASSERT_TRUE(secondCopy);
}
EXPECT_FALSE(cache.set(mAgentHalfExtents, mTilePosition, anotherRecastMesh, mOffMeshConnections,
std::move(anotherNavMeshData)));
EXPECT_TRUE(cache.get(mAgentHalfExtents, mTilePosition, mRecastMesh, mOffMeshConnections));
}
}

@ -0,0 +1,40 @@
#ifndef OPENMW_TEST_SUITE_DETOURNAVIGATOR_OPERATORS_H
#define OPENMW_TEST_SUITE_DETOURNAVIGATOR_OPERATORS_H
#include <components/bullethelpers/operators.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/osghelpers/operators.hpp>
#include <deque>
#include <iomanip>
#include <iostream>
#include <limits>
#include <sstream>
#include <gtest/gtest.h>
namespace DetourNavigator
{
static inline bool operator ==(const TileBounds& lhs, const TileBounds& rhs)
{
return lhs.mMin == rhs.mMin && lhs.mMax == rhs.mMax;
}
}
namespace testing
{
template <>
inline testing::Message& Message::operator <<(const std::deque<osg::Vec3f>& value)
{
(*this) << "{\n";
for (const auto& v : value)
{
std::ostringstream stream;
stream << v;
(*this) << stream.str() << ",\n";
}
return (*this) << "}";
}
}
#endif

@ -0,0 +1,413 @@
#include "operators.hpp"
#include <components/detournavigator/recastmeshbuilder.hpp>
#include <components/detournavigator/settings.hpp>
#include <components/detournavigator/recastmesh.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btBvhTriangleMeshShape.h>
#include <BulletCollision/CollisionShapes/btTriangleMesh.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <gtest/gtest.h>
namespace DetourNavigator
{
static inline bool operator ==(const RecastMesh::Water& lhs, const RecastMesh::Water& rhs)
{
return lhs.mCellSize == rhs.mCellSize && lhs.mTransform == rhs.mTransform;
}
}
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorRecastMeshBuilderTest : Test
{
Settings mSettings;
TileBounds mBounds;
DetourNavigatorRecastMeshBuilderTest()
{
mSettings.mRecastScaleFactor = 1.0f;
mSettings.mTrianglesPerChunk = 256;
mBounds.mMin = osg::Vec2f(-std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon(),
-std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon());
mBounds.mMax = osg::Vec2f(std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon(),
std::numeric_limits<float>::max() * std::numeric_limits<float>::epsilon());
}
};
TEST_F(DetourNavigatorRecastMeshBuilderTest, create_for_empty_should_return_empty)
{
RecastMeshBuilder builder(mSettings, mBounds);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>());
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>());
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>());
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_bhv_triangle_mesh_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_bhv_triangle_mesh_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
2, 3, 0,
0, 3, 4,
0, 3, 0,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_heightfield_terrian_shape)
{
const std::array<btScalar, 4> heightfieldData {{0, 0, 0, 0}};
btHeightfieldTerrainShape shape(2, 2, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
-0.5, 0, -0.5,
-0.5, 0, 0.5,
0.5, 0, -0.5,
0.5, 0, -0.5,
-0.5, 0, 0.5,
0.5, 0, 0.5,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2, 3, 4, 5}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground, AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_box_shape_should_produce_12_triangles)
{
btBoxShape shape(btVector3(1, 1, 2));
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(static_cast<const btCollisionShape&>(shape), btTransform::getIdentity(), AreaType_ground);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 2, 1,
-1, 2, 1,
1, 2, -1,
-1, 2, -1,
1, -2, 1,
-1, -2, 1,
1, -2, -1,
-1, -2, -1,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({
0, 2, 3,
3, 1, 0,
0, 4, 6,
6, 2, 0,
0, 1, 5,
5, 4, 0,
7, 5, 1,
1, 3, 7,
7, 3, 2,
2, 6, 7,
7, 6, 4,
4, 5, 7,
}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>(12, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_compound_shape)
{
btTriangleMesh mesh1;
mesh1.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle1(&mesh1, true);
btBoxShape box(btVector3(1, 1, 2));
btTriangleMesh mesh2;
mesh2.addTriangle(btVector3(1, 1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle2(&mesh2, true);
btCompoundShape shape;
shape.addChildShape(btTransform::getIdentity(), &triangle1);
shape.addChildShape(btTransform::getIdentity(), &box);
shape.addChildShape(btTransform::getIdentity(), &triangle2);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
1, 2, 1,
-1, 2, 1,
1, 2, -1,
-1, 2, -1,
1, -2, 1,
-1, -2, 1,
1, -2, -1,
-1, -2, -1,
1, 0, -1,
-1, 0, 1,
1, 0, 1,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({
0, 1, 2,
3, 5, 6,
6, 4, 3,
3, 7, 9,
9, 5, 3,
3, 4, 8,
8, 7, 3,
10, 8, 4,
4, 6, 10,
10, 6, 5,
5, 9, 10,
10, 9, 7,
7, 8, 10,
11, 12, 13,
}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>(14, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle(&mesh, true);
btCompoundShape shape;
shape.addChildShape(btTransform::getIdentity(), &triangle);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
2, 3, 0,
0, 3, 4,
0, 3, 0,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_transformed_compound_shape_with_transformed_bhv_triangle_shape)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape triangle(&mesh, true);
btCompoundShape shape;
shape.addChildShape(btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
&triangle);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btMatrix3x3::getIdentity().scaled(btVector3(1, 2, 3)), btVector3(1, 2, 3)),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
3, 12, 2,
1, 12, 10,
1, 12, 2,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, without_bounds_add_bhv_triangle_shape_should_not_filter_by_bounds)
{
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
-2, 0, -3,
-3, 0, -2,
-3, 0, -3,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2, 3, 4, 5}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>(2, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_bhv_triangle_shape_should_filter_by_bounds)
{
mSettings.mRecastScaleFactor = 0.1f;
mBounds.mMin = osg::Vec2f(-3, -3) * mSettings.mRecastScaleFactor;
mBounds.mMax = osg::Vec2f(-2, -2) * mSettings.mRecastScaleFactor;
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform::getIdentity(),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
-0.2f, 0, -0.3f,
-0.3f, 0, -0.2f,
-0.3f, 0, -0.3f,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_x_bhv_triangle_shape_should_filter_by_bounds)
{
mBounds.mMin = osg::Vec2f(-5, -5);
mBounds.mMax = osg::Vec2f(5, -2);
btTriangleMesh mesh;
mesh.addTriangle(btVector3(0, -1, -1), btVector3(0, -1, -1), btVector3(0, 1, -1));
mesh.addTriangle(btVector3(0, -3, -3), btVector3(0, -3, -2), btVector3(0, -2, -3));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(1, 0, 0),
static_cast<btScalar>(-osg::PI_4))),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
0, -0.70710659027099609375, -3.535533905029296875,
0, 0.707107067108154296875, -3.535533905029296875,
0, 2.384185791015625e-07, -4.24264049530029296875,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_y_bhv_triangle_shape_should_filter_by_bounds)
{
mBounds.mMin = osg::Vec2f(-5, -5);
mBounds.mMax = osg::Vec2f(-3, 5);
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, 0, -1), btVector3(-1, 0, 1), btVector3(1, 0, -1));
mesh.addTriangle(btVector3(-3, 0, -3), btVector3(-3, 0, -2), btVector3(-2, 0, -3));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(0, 1, 0),
static_cast<btScalar>(osg::PI_4))),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
-3.535533905029296875, -0.70710659027099609375, 0,
-3.535533905029296875, 0.707107067108154296875, 0,
-4.24264049530029296875, 2.384185791015625e-07, 0,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, with_bounds_add_rotated_by_z_bhv_triangle_shape_should_filter_by_bounds)
{
mBounds.mMin = osg::Vec2f(-5, -5);
mBounds.mMax = osg::Vec2f(-1, -1);
btTriangleMesh mesh;
mesh.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
mesh.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape(&mesh, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape),
btTransform(btQuaternion(btVector3(0, 0, 1),
static_cast<btScalar>(osg::PI_4))),
AreaType_ground
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
0.707107067108154296875, 0, -3.535533905029296875,
-0.70710659027099609375, 0, -3.535533905029296875,
2.384185791015625e-07, 0, -4.24264049530029296875,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, flags_values_should_be_corresponding_to_added_objects)
{
btTriangleMesh mesh1;
mesh1.addTriangle(btVector3(-1, -1, 0), btVector3(-1, 1, 0), btVector3(1, -1, 0));
btBvhTriangleMeshShape shape1(&mesh1, true);
btTriangleMesh mesh2;
mesh2.addTriangle(btVector3(-3, -3, 0), btVector3(-3, -2, 0), btVector3(-2, -3, 0));
btBvhTriangleMeshShape shape2(&mesh2, true);
RecastMeshBuilder builder(mSettings, mBounds);
builder.addObject(
static_cast<const btCollisionShape&>(shape1),
btTransform::getIdentity(),
AreaType_ground
);
builder.addObject(
static_cast<const btCollisionShape&>(shape2),
btTransform::getIdentity(),
AreaType_null
);
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getVertices(), std::vector<float>({
1, 0, -1,
-1, 0, 1,
-1, 0, -1,
-2, 0, -3,
-3, 0, -2,
-3, 0, -3,
}));
EXPECT_EQ(recastMesh->getIndices(), std::vector<int>({0, 1, 2, 3, 4, 5}));
EXPECT_EQ(recastMesh->getAreaTypes(), std::vector<AreaType>({AreaType_ground, AreaType_null}));
}
TEST_F(DetourNavigatorRecastMeshBuilderTest, add_water_then_get_water_should_return_it)
{
RecastMeshBuilder builder(mSettings, mBounds);
builder.addWater(1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300)));
const auto recastMesh = builder.create();
EXPECT_EQ(recastMesh->getWater(), std::vector<RecastMesh::Water>({
RecastMesh::Water {1000, btTransform(btMatrix3x3::getIdentity(), btVector3(100, 200, 300))}
}));
}
}

@ -0,0 +1,72 @@
#include "operators.hpp"
#include <components/detournavigator/recastmeshobject.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <gtest/gtest.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorRecastMeshObjectTest : Test
{
btBoxShape mBoxShape {btVector3(1, 2, 3)};
btCompoundShape mCompoundShape {btVector3(1, 2, 3)};
btTransform mTransform {btQuaternion(btVector3(1, 2, 3), 1), btVector3(1, 2, 3)};
DetourNavigatorRecastMeshObjectTest()
{
mCompoundShape.addChildShape(mTransform, std::addressof(mBoxShape));
}
};
TEST_F(DetourNavigatorRecastMeshObjectTest, constructed_object_should_have_shape_and_transform)
{
const RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_EQ(std::addressof(object.getShape()), std::addressof(mBoxShape));
EXPECT_EQ(object.getTransform(), mTransform);
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_same_transform_for_not_compound_shape_should_return_false)
{
RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_FALSE(object.update(mTransform, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_different_transform_should_return_true)
{
RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_TRUE(object.update(btTransform::getIdentity(), AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_with_different_flags_should_return_true)
{
RecastMeshObject object(mBoxShape, mTransform, AreaType_ground);
EXPECT_TRUE(object.update(mTransform, AreaType_null));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_compound_shape_with_same_transform_and_not_changed_child_transform_should_return_false)
{
RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground);
EXPECT_FALSE(object.update(mTransform, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, update_for_compound_shape_with_same_transform_and_changed_child_transform_should_return_true)
{
RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground);
mCompoundShape.updateChildTransform(0, btTransform::getIdentity());
EXPECT_TRUE(object.update(mTransform, AreaType_ground));
}
TEST_F(DetourNavigatorRecastMeshObjectTest, repeated_update_for_compound_shape_without_changes_should_return_false)
{
RecastMeshObject object(mCompoundShape, mTransform, AreaType_ground);
mCompoundShape.updateChildTransform(0, btTransform::getIdentity());
object.update(mTransform, AreaType_ground);
EXPECT_FALSE(object.update(mTransform, AreaType_ground));
}
}

@ -0,0 +1,68 @@
#include "operators.hpp"
#include <components/detournavigator/settingsutils.hpp>
#include <gtest/gtest.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct DetourNavigatorGetTilePositionTest : Test
{
Settings mSettings;
DetourNavigatorGetTilePositionTest()
{
mSettings.mCellSize = 0.5;
mSettings.mTileSize = 64;
}
};
TEST_F(DetourNavigatorGetTilePositionTest, for_zero_coordinates_should_return_zero_tile_position)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(0, 0, 0)), TilePosition(0, 0));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_size_should_be_multiplied_by_cell_size)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(32, 0, 0)), TilePosition(1, 0));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_position_calculates_by_floor)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(31, 0, 0)), TilePosition(0, 0));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_position_depends_on_x_and_z_coordinates)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(32, 64, 128)), TilePosition(1, 4));
}
TEST_F(DetourNavigatorGetTilePositionTest, tile_position_works_for_negative_coordinates)
{
EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(-31, 0, -32)), TilePosition(-1, -1));
}
struct DetourNavigatorMakeTileBoundsTest : Test
{
Settings mSettings;
DetourNavigatorMakeTileBoundsTest()
{
mSettings.mCellSize = 0.5;
mSettings.mTileSize = 64;
}
};
TEST_F(DetourNavigatorMakeTileBoundsTest, tile_bounds_depend_on_tile_size_and_cell_size)
{
EXPECT_EQ(makeTileBounds(mSettings, TilePosition(0, 0)), (TileBounds {osg::Vec2f(0, 0), osg::Vec2f(32, 32)}));
}
TEST_F(DetourNavigatorMakeTileBoundsTest, tile_bounds_are_multiplied_by_tile_position)
{
EXPECT_EQ(makeTileBounds(mSettings, TilePosition(1, 2)), (TileBounds {osg::Vec2f(32, 64), osg::Vec2f(64, 96)}));
}
}

@ -142,6 +142,7 @@ namespace Resource
static bool operator ==(const Resource::BulletShape& lhs, const Resource::BulletShape& rhs)
{
return compareObjects(lhs.mCollisionShape, rhs.mCollisionShape)
&& compareObjects(lhs.mAvoidCollisionShape, rhs.mAvoidCollisionShape)
&& lhs.mCollisionBoxHalfExtents == rhs.mCollisionBoxHalfExtents
&& lhs.mCollisionBoxTranslate == rhs.mCollisionBoxTranslate
&& lhs.mAnimatedShapes == rhs.mAnimatedShapes;
@ -151,6 +152,7 @@ namespace Resource
{
return stream << "Resource::BulletShape {"
<< value.mCollisionShape << ", "
<< value.mAvoidCollisionShape << ", "
<< value.mCollisionBoxHalfExtents << ", "
<< value.mAnimatedShapes
<< "}";
@ -266,7 +268,8 @@ namespace
value.target = Nif::ControlledPtr(nullptr);
}
void copy(const btTransform& src, Nif::Transformation& dst) {
void copy(const btTransform& src, Nif::Transformation& dst)
{
dst.pos = osg::Vec3f(src.getOrigin().x(), src.getOrigin().y(), src.getOrigin().z());
for (int row = 0; row < 3; ++row)
for (int column = 0; column < 3; ++column)
@ -837,7 +840,10 @@ namespace
EXPECT_CALL(mNifFile, getFilename()).WillOnce(Return("test.nif"));
const auto result = mLoader.load(mNifFile);
std::unique_ptr<btTriangleMesh> triangles(new btTriangleMesh(false));
triangles->addTriangle(btVector3(0, 0, 0), btVector3(1, 0, 0), btVector3(1, 1, 0));
Resource::BulletShape expected;
expected.mAvoidCollisionShape = new Resource::TriangleMeshShape(triangles.release(), false);
EXPECT_EQ(*result, expected);
}

@ -68,7 +68,7 @@ test: off
#notifications:
# - provider: Email
# to:
# -
# -
# on_build_failure: true
# on_build_status_changed: true

@ -0,0 +1,40 @@
find_path(RecastNavigation_INCLUDE_DIR
NAMES Recast.h
HINTS $ENV{RecastNavigation_ROOT}
${RecastNavigation_ROOT}
PATH_SUFFIXES include
)
mark_as_advanced(RecastNavigation_INCLUDE_DIR)
include(FindPackageHandleStandardArgs)
set(RecastNavigation_LIBRARIES "")
foreach(COMPONENT ${RecastNavigation_FIND_COMPONENTS})
if(NOT RecastNavigation_${COMPONENT}_FOUND)
find_library(RecastNavigation_${COMPONENT}_LIBRARY
HINTS $ENV{RecastNavigation_ROOT}
${RecastNavigation_ROOT}
NAMES ${COMPONENT}
PATH_SUFFIXES lib
)
find_package_handle_standard_args(RecastNavigation_${COMPONENT} DEFAULT_MSG
RecastNavigation_${COMPONENT}_LIBRARY
RecastNavigation_INCLUDE_DIR
)
mark_as_advanced(RecastNavigation_${COMPONENT}_LIBRARY)
if(RecastNavigation_${COMPONENT}_FOUND)
list(APPEND RecastNavigation_LIBRARIES ${RecastNavigation_${COMPONENT}_LIBRARY})
endif()
endif()
endforeach()
mark_as_advanced(RecastNavigation_LIBRARIES)
find_package_handle_standard_args(RecastNavigation DEFAULT_MSG
RecastNavigation_LIBRARIES
RecastNavigation_INCLUDE_DIR
)
if(RecastNavigation_FOUND)
set(RecastNavigation_INCLUDE_DIRS ${RecastNavigation_INCLUDE_DIR})
endif()

@ -51,7 +51,7 @@ add_component_dir (shader
add_component_dir (sceneutil
clone attach visitor util statesetupdater controller skeleton riggeometry morphgeometry lightcontroller
lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil writescene serialize optimizer
actorutil
actorutil detourdebugdraw navmesh agentpath
)
add_component_dir (nif
@ -156,6 +156,23 @@ if(NOT WIN32 AND NOT ANDROID)
)
endif()
add_component_dir(detournavigator
debug
makenavmesh
findsmoothpath
recastmeshbuilder
recastmeshmanager
cachedrecastmeshmanager
navmeshmanager
navigator
asyncnavmeshupdater
chunkytrimesh
recastmesh
tilecachedrecastmeshmanager
recastmeshobject
navmeshtilescache
)
set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui
)
@ -196,6 +213,10 @@ include_directories(${Bullet_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR})
find_package(RecastNavigation COMPONENTS DebugUtils Detour Recast REQUIRED)
include_directories(SYSTEM ${RecastNavigation_INCLUDE_DIRS})
target_link_libraries(components
${Boost_SYSTEM_LIBRARY}
${Boost_FILESYSTEM_LIBRARY}
@ -214,6 +235,7 @@ target_link_libraries(components
${SDL2_LIBRARIES}
${OPENGL_gl_LIBRARY}
${MyGUI_LIBRARIES}
${RecastNavigation_LIBRARIES}
)
if (WIN32)

@ -0,0 +1,71 @@
#ifndef OPENMW_COMPONENTS_BULLETHELPERS_OPERATORS_H
#define OPENMW_COMPONENTS_BULLETHELPERS_OPERATORS_H
#include <iomanip>
#include <limits>
#include <ostream>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <LinearMath/btTransform.h>
inline std::ostream& operator <<(std::ostream& stream, const btVector3& value)
{
return stream << "btVector3(" << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.x()
<< ", " << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.y()
<< ", " << std::setprecision(std::numeric_limits<float>::max_exponent10) << value.z()
<< ')';
}
inline std::ostream& operator <<(std::ostream& stream, BroadphaseNativeTypes value)
{
switch (value)
{
#ifndef SHAPE_NAME
#define SHAPE_NAME(name) case name: return stream << #name;
SHAPE_NAME(BOX_SHAPE_PROXYTYPE)
SHAPE_NAME(TRIANGLE_SHAPE_PROXYTYPE)
SHAPE_NAME(TETRAHEDRAL_SHAPE_PROXYTYPE)
SHAPE_NAME(CONVEX_TRIANGLEMESH_SHAPE_PROXYTYPE)
SHAPE_NAME(CONVEX_HULL_SHAPE_PROXYTYPE)
SHAPE_NAME(CONVEX_POINT_CLOUD_SHAPE_PROXYTYPE)
SHAPE_NAME(CUSTOM_POLYHEDRAL_SHAPE_TYPE)
SHAPE_NAME(IMPLICIT_CONVEX_SHAPES_START_HERE)
SHAPE_NAME(SPHERE_SHAPE_PROXYTYPE)
SHAPE_NAME(MULTI_SPHERE_SHAPE_PROXYTYPE)
SHAPE_NAME(CAPSULE_SHAPE_PROXYTYPE)
SHAPE_NAME(CONE_SHAPE_PROXYTYPE)
SHAPE_NAME(CONVEX_SHAPE_PROXYTYPE)
SHAPE_NAME(CYLINDER_SHAPE_PROXYTYPE)
SHAPE_NAME(UNIFORM_SCALING_SHAPE_PROXYTYPE)
SHAPE_NAME(MINKOWSKI_SUM_SHAPE_PROXYTYPE)
SHAPE_NAME(MINKOWSKI_DIFFERENCE_SHAPE_PROXYTYPE)
SHAPE_NAME(BOX_2D_SHAPE_PROXYTYPE)
SHAPE_NAME(CONVEX_2D_SHAPE_PROXYTYPE)
SHAPE_NAME(CUSTOM_CONVEX_SHAPE_TYPE)
SHAPE_NAME(CONCAVE_SHAPES_START_HERE)
SHAPE_NAME(TRIANGLE_MESH_SHAPE_PROXYTYPE)
SHAPE_NAME(SCALED_TRIANGLE_MESH_SHAPE_PROXYTYPE)
SHAPE_NAME(FAST_CONCAVE_MESH_PROXYTYPE)
SHAPE_NAME(TERRAIN_SHAPE_PROXYTYPE)
SHAPE_NAME(GIMPACT_SHAPE_PROXYTYPE)
SHAPE_NAME(MULTIMATERIAL_TRIANGLE_MESH_PROXYTYPE)
SHAPE_NAME(EMPTY_SHAPE_PROXYTYPE)
SHAPE_NAME(STATIC_PLANE_PROXYTYPE)
SHAPE_NAME(CUSTOM_CONCAVE_SHAPE_TYPE)
SHAPE_NAME(CONCAVE_SHAPES_END_HERE)
SHAPE_NAME(COMPOUND_SHAPE_PROXYTYPE)
SHAPE_NAME(SOFTBODY_SHAPE_PROXYTYPE)
SHAPE_NAME(HFFLUID_SHAPE_PROXYTYPE)
SHAPE_NAME(HFFLUID_BUOYANT_CONVEX_SHAPE_PROXYTYPE)
SHAPE_NAME(INVALID_SHAPE_PROXYTYPE)
SHAPE_NAME(MAX_BROADPHASE_COLLISION_TYPES)
#undef SHAPE_NAME
#endif
default:
return stream << "undefined(" << static_cast<int>(value) << ")";
}
}
#endif

@ -320,6 +320,9 @@ namespace Compiler
extensions.registerInstruction ("removefromlevitem", "ccl", opcodeRemoveFromLevItem);
extensions.registerInstruction ("tb", "", opcodeToggleBorders);
extensions.registerInstruction ("toggleborders", "", opcodeToggleBorders);
extensions.registerInstruction ("togglenavmesh", "", opcodeToggleNavMesh);
extensions.registerInstruction ("toggleactorspaths", "", opcodeToggleActorsPaths);
extensions.registerInstruction ("setnavmeshnumber", "l", opcodeSetNavMeshNumberToRender);
}
}

@ -296,6 +296,9 @@ namespace Compiler
const int opcodeShowSceneGraph = 0x2002f;
const int opcodeShowSceneGraphExplicit = 0x20030;
const int opcodeToggleBorders = 0x2000307;
const int opcodeToggleNavMesh = 0x2000308;
const int opcodeToggleActorsPaths = 0x2000309;
const int opcodeSetNavMeshNumberToRender = 0x200030a;
}
namespace Sky

@ -0,0 +1,16 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_AREATYPE_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_AREATYPE_H
#include <Recast.h>
namespace DetourNavigator
{
enum AreaType : unsigned char
{
AreaType_null = RC_NULL_AREA,
AreaType_water,
AreaType_ground = RC_WALKABLE_AREA,
};
}
#endif

@ -0,0 +1,198 @@
#include "asyncnavmeshupdater.hpp"
#include "debug.hpp"
#include "makenavmesh.hpp"
#include "settings.hpp"
#include <components/debug/debuglog.hpp>
#include <iostream>
namespace
{
using DetourNavigator::ChangeType;
using DetourNavigator::TilePosition;
int getManhattanDistance(const TilePosition& lhs, const TilePosition& rhs)
{
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
{
static std::ostream& operator <<(std::ostream& stream, UpdateNavMeshStatus value)
{
switch (value)
{
case UpdateNavMeshStatus::ignore:
return stream << "ignore";
case UpdateNavMeshStatus::removed:
return stream << "removed";
case UpdateNavMeshStatus::add:
return stream << "add";
case UpdateNavMeshStatus::replaced:
return stream << "replaced";
}
return stream << "unknown";
}
AsyncNavMeshUpdater::AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager,
OffMeshConnectionsManager& offMeshConnectionsManager)
: mSettings(settings)
, mRecastMeshManager(recastMeshManager)
, mOffMeshConnectionsManager(offMeshConnectionsManager)
, mShouldStop()
, mNavMeshTilesCache(settings.mMaxNavMeshTilesCacheSize)
{
for (std::size_t i = 0; i < mSettings.get().mAsyncNavMeshUpdaterThreads; ++i)
mThreads.emplace_back([&] { process(); });
}
AsyncNavMeshUpdater::~AsyncNavMeshUpdater()
{
mShouldStop = true;
std::unique_lock<std::mutex> lock(mMutex);
mJobs = decltype(mJobs)();
mHasJob.notify_all();
lock.unlock();
for (auto& thread : mThreads)
thread.join();
}
void AsyncNavMeshUpdater::post(const osg::Vec3f& agentHalfExtents,
const SharedNavMeshCacheItem& navMeshCacheItem, const TilePosition& playerTile,
const std::map<TilePosition, ChangeType>& changedTiles)
{
*mPlayerTile.lock() = playerTile;
if (changedTiles.empty())
return;
const std::lock_guard<std::mutex> lock(mMutex);
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)});
}
log("posted ", mJobs.size(), " jobs");
mHasJob.notify_all();
}
void AsyncNavMeshUpdater::wait()
{
std::unique_lock<std::mutex> lock(mMutex);
mDone.wait(lock, [&] { return mJobs.empty(); });
}
void AsyncNavMeshUpdater::process() throw()
{
log("start process jobs");
while (!mShouldStop)
{
try
{
if (const auto job = getNextJob())
processJob(*job);
}
catch (const std::exception& e)
{
DetourNavigator::log("AsyncNavMeshUpdater::process exception: ", e.what());
}
}
log("stop process jobs");
}
void AsyncNavMeshUpdater::processJob(const Job& job)
{
log("process job for agent=", job.mAgentHalfExtents);
const auto start = std::chrono::steady_clock::now();
const auto firstStart = setFirstStart(start);
const auto recastMesh = mRecastMeshManager.get().getMesh(job.mChangedTile);
const auto playerTile = *mPlayerTile.lockConst();
const auto offMeshConnections = mOffMeshConnectionsManager.get().get(job.mChangedTile);
const auto status = updateNavMesh(job.mAgentHalfExtents, recastMesh.get(), job.mChangedTile, playerTile,
offMeshConnections, mSettings, job.mNavMeshCacheItem, mNavMeshTilesCache);
const auto finish = std::chrono::steady_clock::now();
writeDebugFiles(job, recastMesh.get());
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");
}
boost::optional<AsyncNavMeshUpdater::Job> AsyncNavMeshUpdater::getNextJob()
{
std::unique_lock<std::mutex> lock(mMutex);
if (mJobs.empty())
mHasJob.wait_for(lock, std::chrono::milliseconds(10));
if (mJobs.empty())
{
mFirstStart.lock()->reset();
mDone.notify_all();
return boost::none;
}
log("got ", mJobs.size(), " jobs");
const auto job = mJobs.top();
mJobs.pop();
const auto pushed = mPushed.find(job.mAgentHalfExtents);
pushed->second.erase(job.mChangedTile);
if (pushed->second.empty())
mPushed.erase(pushed);
return job;
}
void AsyncNavMeshUpdater::writeDebugFiles(const Job& job, const RecastMesh* recastMesh) const
{
std::string revision;
std::string recastMeshRevision;
std::string navMeshRevision;
if ((mSettings.get().mEnableWriteNavMeshToFile || mSettings.get().mEnableWriteRecastMeshToFile)
&& (mSettings.get().mEnableRecastMeshFileNameRevision || mSettings.get().mEnableNavMeshFileNameRevision))
{
revision = "." + std::to_string((std::chrono::steady_clock::now()
- std::chrono::steady_clock::time_point()).count());
if (mSettings.get().mEnableRecastMeshFileNameRevision)
recastMeshRevision = revision;
if (mSettings.get().mEnableNavMeshFileNameRevision)
navMeshRevision = revision;
}
if (recastMesh && mSettings.get().mEnableWriteRecastMeshToFile)
writeToFile(*recastMesh, mSettings.get().mRecastMeshPathPrefix + std::to_string(job.mChangedTile.x())
+ "_" + std::to_string(job.mChangedTile.y()) + "_", recastMeshRevision);
if (mSettings.get().mEnableWriteNavMeshToFile)
writeToFile(job.mNavMeshCacheItem.lockConst()->getValue(), mSettings.get().mNavMeshPathPrefix, navMeshRevision);
}
std::chrono::steady_clock::time_point AsyncNavMeshUpdater::setFirstStart(const std::chrono::steady_clock::time_point& value)
{
const auto locked = mFirstStart.lock();
if (!*locked)
*locked = value;
return *locked.get();
}
}

@ -0,0 +1,88 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_ASYNCNAVMESHUPDATER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_ASYNCNAVMESHUPDATER_H
#include "navmeshcacheitem.hpp"
#include "offmeshconnectionsmanager.hpp"
#include "tilecachedrecastmeshmanager.hpp"
#include "tileposition.hpp"
#include "navmeshtilescache.hpp"
#include <osg/Vec3f>
#include <boost/optional.hpp>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <set>
#include <thread>
class dtNavMesh;
namespace DetourNavigator
{
enum class ChangeType
{
remove = 0,
mixed = 1,
add = 2,
};
class AsyncNavMeshUpdater
{
public:
AsyncNavMeshUpdater(const Settings& settings, TileCachedRecastMeshManager& recastMeshManager,
OffMeshConnectionsManager& offMeshConnectionsManager);
~AsyncNavMeshUpdater();
void post(const osg::Vec3f& agentHalfExtents, const SharedNavMeshCacheItem& mNavMeshCacheItem,
const TilePosition& playerTile, const std::map<TilePosition, ChangeType>& changedTiles);
void wait();
private:
struct Job
{
osg::Vec3f mAgentHalfExtents;
SharedNavMeshCacheItem mNavMeshCacheItem;
TilePosition mChangedTile;
std::tuple<ChangeType, int, int> mPriority;
friend inline bool operator <(const Job& lhs, const Job& rhs)
{
return lhs.mPriority > rhs.mPriority;
}
};
using Jobs = std::priority_queue<Job, std::deque<Job>>;
std::reference_wrapper<const Settings> mSettings;
std::reference_wrapper<TileCachedRecastMeshManager> mRecastMeshManager;
std::reference_wrapper<OffMeshConnectionsManager> mOffMeshConnectionsManager;
std::atomic_bool mShouldStop;
std::mutex mMutex;
std::condition_variable mHasJob;
std::condition_variable mDone;
Jobs mJobs;
std::map<osg::Vec3f, std::set<TilePosition>> mPushed;
Misc::ScopeGuarded<TilePosition> mPlayerTile;
Misc::ScopeGuarded<boost::optional<std::chrono::steady_clock::time_point>> mFirstStart;
NavMeshTilesCache mNavMeshTilesCache;
std::vector<std::thread> mThreads;
void process() throw();
void 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);
};
}
#endif

@ -0,0 +1,20 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_BOUNDS_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_BOUNDS_H
#include <osg/Vec3f>
namespace DetourNavigator
{
struct Bounds
{
osg::Vec3f mMin;
osg::Vec3f mMax;
};
inline bool isEmpty(const Bounds& value)
{
return value.mMin == value.mMax;
}
}
#endif

@ -0,0 +1,63 @@
#include "cachedrecastmeshmanager.hpp"
#include "debug.hpp"
namespace DetourNavigator
{
CachedRecastMeshManager::CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds)
: mImpl(settings, bounds)
{}
bool CachedRecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape,
const btTransform& transform, const AreaType areaType)
{
if (!mImpl.addObject(id, shape, transform, areaType))
return false;
mCached.reset();
return true;
}
bool CachedRecastMeshManager::updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType)
{
if (!mImpl.updateObject(id, transform, areaType))
return false;
mCached.reset();
return true;
}
boost::optional<RemovedRecastMeshObject> CachedRecastMeshManager::removeObject(const ObjectId id)
{
const auto object = mImpl.removeObject(id);
if (object)
mCached.reset();
return object;
}
bool CachedRecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize,
const btTransform& transform)
{
if (!mImpl.addWater(cellPosition, cellSize, transform))
return false;
mCached.reset();
return true;
}
boost::optional<RecastMeshManager::Water> CachedRecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
{
const auto water = mImpl.removeWater(cellPosition);
if (water)
mCached.reset();
return water;
}
std::shared_ptr<RecastMesh> CachedRecastMeshManager::getMesh()
{
if (!mCached)
mCached = mImpl.getMesh();
return mCached;
}
bool CachedRecastMeshManager::isEmpty() const
{
return mImpl.isEmpty();
}
}

@ -0,0 +1,36 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_CACHEDRECASTMESHMANAGER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_CACHEDRECASTMESHMANAGER_H
#include "recastmeshmanager.hpp"
#include <boost/optional.hpp>
namespace DetourNavigator
{
class CachedRecastMeshManager
{
public:
CachedRecastMeshManager(const Settings& settings, const TileBounds& bounds);
bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType);
bool updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType);
bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform);
boost::optional<RecastMeshManager::Water> removeWater(const osg::Vec2i& cellPosition);
boost::optional<RemovedRecastMeshObject> removeObject(const ObjectId id);
std::shared_ptr<RecastMesh> getMesh();
bool isEmpty() const;
private:
RecastMeshManager mImpl;
std::shared_ptr<RecastMesh> mCached;
};
}
#endif

@ -0,0 +1,179 @@
#include "chunkytrimesh.hpp"
#include "exceptions.hpp"
#include <osg/Vec2f>
#include <algorithm>
namespace DetourNavigator
{
namespace
{
struct BoundsItem
{
Rect mBounds;
std::ptrdiff_t mOffset;
unsigned char mAreaTypes;
};
template <std::size_t axis>
struct LessBoundsItem
{
bool operator ()(const BoundsItem& lhs, const BoundsItem& rhs) const
{
return lhs.mBounds.mMinBound[axis] < rhs.mBounds.mMinBound[axis];
}
};
void calcExtends(const std::vector<BoundsItem>& items, const std::size_t imin, const std::size_t imax,
Rect& bounds)
{
bounds = items[imin].mBounds;
std::for_each(
items.begin() + static_cast<std::ptrdiff_t>(imin) + 1,
items.begin() + static_cast<std::ptrdiff_t>(imax),
[&] (const BoundsItem& item)
{
for (int i = 0; i < 2; ++i)
{
bounds.mMinBound[i] = std::min(bounds.mMinBound[i], item.mBounds.mMinBound[i]);
bounds.mMaxBound[i] = std::max(bounds.mMaxBound[i], item.mBounds.mMaxBound[i]);
}
});
}
void subdivide(std::vector<BoundsItem>& items, const std::size_t imin, const std::size_t imax,
const std::size_t trisPerChunk, const std::vector<int>& inIndices, const std::vector<AreaType>& inAreaTypes,
std::size_t& curNode, std::vector<ChunkyTriMeshNode>& nodes, std::size_t& curTri,
std::vector<int>& outIndices, std::vector<AreaType>& outAreaTypes)
{
const auto inum = imax - imin;
const auto icur = curNode;
if (curNode > nodes.size())
return;
ChunkyTriMeshNode& node = nodes[curNode++];
if (inum <= trisPerChunk)
{
// Leaf
calcExtends(items, imin, imax, node.mBounds);
// Copy triangles.
node.mOffset = static_cast<std::ptrdiff_t>(curTri);
node.mSize = inum;
for (std::size_t i = imin; i < imax; ++i)
{
std::copy(
inIndices.begin() + items[i].mOffset * 3,
inIndices.begin() + items[i].mOffset * 3 + 3,
outIndices.begin() + static_cast<std::ptrdiff_t>(curTri) * 3
);
outAreaTypes[curTri] = inAreaTypes[static_cast<std::size_t>(items[i].mOffset)];
curTri++;
}
}
else
{
// Split
calcExtends(items, imin, imax, node.mBounds);
if (node.mBounds.mMaxBound.x() - node.mBounds.mMinBound.x()
>= node.mBounds.mMaxBound.y() - node.mBounds.mMinBound.y())
{
// Sort along x-axis
std::sort(
items.begin() + static_cast<std::ptrdiff_t>(imin),
items.begin() + static_cast<std::ptrdiff_t>(imax),
LessBoundsItem<0> {}
);
}
else
{
// Sort along y-axis
std::sort(
items.begin() + static_cast<std::ptrdiff_t>(imin),
items.begin() + static_cast<std::ptrdiff_t>(imax),
LessBoundsItem<1> {}
);
}
const auto isplit = imin + inum / 2;
// Left
subdivide(items, imin, isplit, trisPerChunk, inIndices, inAreaTypes, curNode, nodes, curTri, outIndices, outAreaTypes);
// Right
subdivide(items, isplit, imax, trisPerChunk, inIndices, inAreaTypes, curNode, nodes, curTri, outIndices, outAreaTypes);
const auto iescape = static_cast<std::ptrdiff_t>(curNode) - static_cast<std::ptrdiff_t>(icur);
// Negative index means escape.
node.mOffset = -iescape;
}
}
}
ChunkyTriMesh::ChunkyTriMesh(const std::vector<float>& verts, const std::vector<int>& indices,
const std::vector<AreaType>& flags, const std::size_t trisPerChunk)
: mMaxTrisPerChunk(0)
{
const auto trianglesCount = indices.size() / 3;
if (trianglesCount == 0)
return;
const auto nchunks = (trianglesCount + trisPerChunk - 1) / trisPerChunk;
mNodes.resize(nchunks * 4);
mIndices.resize(trianglesCount * 3);
mAreaTypes.resize(trianglesCount);
// Build tree
std::vector<BoundsItem> items(trianglesCount);
for (std::size_t i = 0; i < trianglesCount; i++)
{
auto& item = items[i];
item.mOffset = static_cast<std::ptrdiff_t>(i);
item.mAreaTypes = flags[i];
// Calc triangle XZ bounds.
const auto baseIndex = static_cast<std::size_t>(indices[i * 3]) * 3;
item.mBounds.mMinBound.x() = item.mBounds.mMaxBound.x() = verts[baseIndex + 0];
item.mBounds.mMinBound.y() = item.mBounds.mMaxBound.y() = verts[baseIndex + 2];
for (std::size_t j = 1; j < 3; ++j)
{
const auto index = static_cast<std::size_t>(indices[i * 3 + j]) * 3;
item.mBounds.mMinBound.x() = std::min(item.mBounds.mMinBound.x(), verts[index + 0]);
item.mBounds.mMinBound.y() = std::min(item.mBounds.mMinBound.y(), verts[index + 2]);
item.mBounds.mMaxBound.x() = std::max(item.mBounds.mMaxBound.x(), verts[index + 0]);
item.mBounds.mMaxBound.y() = std::max(item.mBounds.mMaxBound.y(), verts[index + 2]);
}
}
std::size_t curTri = 0;
std::size_t curNode = 0;
subdivide(items, 0, trianglesCount, trisPerChunk, indices, flags, curNode, mNodes, curTri, mIndices, mAreaTypes);
items.clear();
mNodes.resize(curNode);
// Calc max tris per node.
for (auto& node : mNodes)
{
const bool isLeaf = node.mOffset >= 0;
if (!isLeaf)
continue;
if (node.mSize > mMaxTrisPerChunk)
mMaxTrisPerChunk = node.mSize;
}
}
}

@ -0,0 +1,99 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_CHUNKYTRIMESH_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_CHUNKYTRIMESH_H
#include "areatype.hpp"
#include <osg/Vec2f>
#include <array>
#include <vector>
namespace DetourNavigator
{
struct Rect
{
osg::Vec2f mMinBound;
osg::Vec2f mMaxBound;
};
struct ChunkyTriMeshNode
{
Rect mBounds;
std::ptrdiff_t mOffset;
std::size_t mSize;
};
struct Chunk
{
const int* const mIndices;
const AreaType* const mAreaTypes;
const std::size_t mSize;
};
inline bool checkOverlapRect(const Rect& lhs, const Rect& rhs)
{
bool overlap = true;
overlap = (lhs.mMinBound.x() > rhs.mMaxBound.x() || lhs.mMaxBound.x() < rhs.mMinBound.x()) ? false : overlap;
overlap = (lhs.mMinBound.y() > rhs.mMaxBound.y() || lhs.mMaxBound.y() < rhs.mMinBound.y()) ? false : overlap;
return overlap;
}
class ChunkyTriMesh
{
public:
/// Creates partitioned triangle mesh (AABB tree),
/// where each node contains at max trisPerChunk triangles.
ChunkyTriMesh(const std::vector<float>& verts, const std::vector<int>& tris,
const std::vector<AreaType>& flags, const std::size_t trisPerChunk);
ChunkyTriMesh(const ChunkyTriMesh&) = delete;
ChunkyTriMesh& operator=(const ChunkyTriMesh&) = delete;
/// Returns the chunk indices which overlap the input rectable.
template <class OutputIterator>
void getChunksOverlappingRect(const Rect& rect, OutputIterator out) const
{
// Traverse tree
for (std::size_t i = 0; i < mNodes.size(); )
{
const ChunkyTriMeshNode* node = &mNodes[i];
const bool overlap = checkOverlapRect(rect, node->mBounds);
const bool isLeafNode = node->mOffset >= 0;
if (isLeafNode && overlap)
*out++ = i;
if (overlap || isLeafNode)
i++;
else
{
const auto escapeIndex = -node->mOffset;
i += static_cast<std::size_t>(escapeIndex);
}
}
}
Chunk getChunk(const std::size_t chunkId) const
{
const auto& node = mNodes[chunkId];
return Chunk {
mIndices.data() + node.mOffset * 3,
mAreaTypes.data() + node.mOffset,
node.mSize
};
}
std::size_t getMaxTrisPerChunk() const
{
return mMaxTrisPerChunk;
}
private:
std::vector<ChunkyTriMeshNode> mNodes;
std::vector<int> mIndices;
std::vector<AreaType> mAreaTypes;
std::size_t mMaxTrisPerChunk;
};
}
#endif

@ -0,0 +1,102 @@
#include "debug.hpp"
#include "exceptions.hpp"
#include "recastmesh.hpp"
#include <DetourNavMesh.h>
#include <fstream>
namespace DetourNavigator
{
void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision)
{
const auto path = pathPrefix + "recastmesh" + revision + ".obj";
std::ofstream file(path);
if (!file.is_open())
throw NavigatorException("Open file failed: " + path);
file.exceptions(std::ios::failbit | std::ios::badbit);
file.precision(std::numeric_limits<float>::max_exponent10);
std::size_t count = 0;
for (auto v : recastMesh.getVertices())
{
if (count % 3 == 0)
{
if (count != 0)
file << '\n';
file << 'v';
}
file << ' ' << v;
++count;
}
file << '\n';
count = 0;
for (auto v : recastMesh.getIndices())
{
if (count % 3 == 0)
{
if (count != 0)
file << '\n';
file << 'f';
}
file << ' ' << (v + 1);
++count;
}
file << '\n';
}
void writeToFile(const dtNavMesh& navMesh, const std::string& pathPrefix, const std::string& revision)
{
const int navMeshSetMagic = 'M' << 24 | 'S' << 16 | 'E' << 8 | 'T'; //'MSET';
const int navMeshSetVersion = 1;
struct NavMeshSetHeader
{
int magic;
int version;
int numTiles;
dtNavMeshParams params;
};
struct NavMeshTileHeader
{
dtTileRef tileRef;
int dataSize;
};
const auto path = pathPrefix + "all_tiles_navmesh" + revision + ".bin";
std::ofstream file(path, std::ios::binary);
if (!file.is_open())
throw NavigatorException("Open file failed: " + path);
file.exceptions(std::ios::failbit | std::ios::badbit);
NavMeshSetHeader header;
header.magic = navMeshSetMagic;
header.version = navMeshSetVersion;
header.numTiles = 0;
for (int i = 0; i < navMesh.getMaxTiles(); ++i)
{
const auto tile = navMesh.getTile(i);
if (!tile || !tile->header || !tile->dataSize)
continue;
header.numTiles++;
}
header.params = *navMesh.getParams();
using const_char_ptr = const char*;
file.write(const_char_ptr(&header), sizeof(header));
for (int i = 0; i < navMesh.getMaxTiles(); ++i)
{
const auto tile = navMesh.getTile(i);
if (!tile || !tile->header || !tile->dataSize)
continue;
NavMeshTileHeader tileHeader;
tileHeader.tileRef = navMesh.getTileRef(tile);
tileHeader.dataSize = tile->dataSize;
file.write(const_char_ptr(&tileHeader), sizeof(tileHeader));
file.write(const_char_ptr(tile->data), tile->dataSize);
}
}
}

@ -0,0 +1,133 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DEBUG_H
#include "tilebounds.hpp"
#include <components/bullethelpers/operators.hpp>
#include <components/misc/guarded.hpp>
#include <components/osghelpers/operators.hpp>
#include <chrono>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <thread>
class dtNavMesh;
namespace DetourNavigator
{
inline std::ostream& operator <<(std::ostream& stream, const TileBounds& value)
{
return stream << "TileBounds {" << value.mMin << ", " << value.mMax << "}";
}
class RecastMesh;
inline std::ostream& operator <<(std::ostream& stream, const std::chrono::steady_clock::time_point& value)
{
using float_s = std::chrono::duration<float, std::ratio<1>>;
return stream << std::fixed << std::setprecision(4)
<< std::chrono::duration_cast<float_s>(value.time_since_epoch()).count();
}
struct Sink
{
virtual ~Sink() = default;
virtual void write(const std::string& text) = 0;
};
class FileSink final : public Sink
{
public:
FileSink(std::string path)
: mPath(std::move(path))
{
mFile.exceptions(std::ios::failbit | std::ios::badbit);
}
void write(const std::string& text) override
{
if (!mFile.is_open())
{
mFile.open(mPath);
}
mFile << text << std::flush;
}
private:
std::string mPath;
std::ofstream mFile;
};
class StdoutSink final : public Sink
{
public:
void write(const std::string& text) override
{
std::cout << text << std::flush;
}
};
class Log
{
public:
void setSink(std::unique_ptr<Sink> sink)
{
*mSink.lock() = std::move(sink);
}
bool isEnabled()
{
return bool(*mSink.lockConst());
}
void write(const std::string& text)
{
const auto sink = mSink.lock();
if (*sink)
sink.get()->write(text);
}
static Log& instance()
{
static Log value;
return value;
}
private:
Misc::ScopeGuarded<std::unique_ptr<Sink>> mSink;
};
inline void write(std::ostream& stream)
{
stream << '\n';
}
template <class Head, class ... Tail>
void write(std::ostream& stream, const Head& head, const Tail& ... tail)
{
stream << head;
write(stream, tail ...);
}
template <class ... Ts>
void log(Ts&& ... values)
{
auto& log = Log::instance();
if (!log.isEnabled())
return;
std::ostringstream stream;
stream << '[' << std::chrono::steady_clock::now() << "] [" << std::this_thread::get_id() << "] ";
write(stream, std::forward<Ts>(values) ...);
log.write(stream.str());
}
void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision);
void writeToFile(const dtNavMesh& navMesh, const std::string& pathPrefix, const std::string& revision);
}
#endif

@ -0,0 +1,38 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_DTSTATUS_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_DTSTATUS_H
#include <DetourStatus.h>
#include <sstream>
#include <vector>
namespace DetourNavigator
{
struct WriteDtStatus
{
dtStatus status;
};
static const std::vector<std::pair<const dtStatus, const char* const>> dtStatuses {
{DT_FAILURE, "DT_FAILURE"},
{DT_SUCCESS, "DT_SUCCESS"},
{DT_IN_PROGRESS, "DT_IN_PROGRESS"},
{DT_WRONG_MAGIC, "DT_WRONG_MAGIC"},
{DT_WRONG_VERSION, "DT_WRONG_VERSION"},
{DT_OUT_OF_MEMORY, "DT_OUT_OF_MEMORY"},
{DT_INVALID_PARAM, "DT_INVALID_PARAM"},
{DT_BUFFER_TOO_SMALL, "DT_BUFFER_TOO_SMALL"},
{DT_OUT_OF_NODES, "DT_OUT_OF_NODES"},
{DT_PARTIAL_RESULT, "DT_PARTIAL_RESULT"},
};
inline std::ostream& operator <<(std::ostream& stream, const WriteDtStatus& value)
{
for (const auto& status : dtStatuses)
if (value.status & status.first)
stream << status.second << " ";
return stream;
}
}
#endif

@ -0,0 +1,21 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_EXCEPTIONS_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_EXCEPTIONS_H
#include <stdexcept>
namespace DetourNavigator
{
struct NavigatorException : std::runtime_error
{
NavigatorException(const std::string& message) : std::runtime_error(message) {}
NavigatorException(const char* message) : std::runtime_error(message) {}
};
struct InvalidArgument : std::invalid_argument
{
InvalidArgument(const std::string& message) : std::invalid_argument(message) {}
InvalidArgument(const char* message) : std::invalid_argument(message) {}
};
}
#endif

@ -0,0 +1,143 @@
#include "findsmoothpath.hpp"
#include <algorithm>
#include <array>
namespace DetourNavigator
{
std::vector<dtPolyRef> fixupCorridor(const std::vector<dtPolyRef>& path, const std::vector<dtPolyRef>& visited)
{
std::vector<dtPolyRef>::const_reverse_iterator furthestVisited;
// Find furthest common polygon.
const auto it = std::find_if(path.rbegin(), path.rend(), [&] (dtPolyRef pathValue)
{
const auto it = std::find(visited.rbegin(), visited.rend(), pathValue);
if (it == visited.rend())
return false;
furthestVisited = it;
return true;
});
// If no intersection found just return current path.
if (it == path.rend())
return path;
const auto furthestPath = it.base() - 1;
// Concatenate paths.
// visited: a_1 ... a_n x b_1 ... b_n
// furthestVisited ^
// path: C x D
// ^ furthestPath
// result: x b_n ... b_1 D
std::vector<dtPolyRef> result;
result.reserve(static_cast<std::size_t>(furthestVisited - visited.rbegin())
+ static_cast<std::size_t>(path.end() - furthestPath) - 1);
std::copy(visited.rbegin(), furthestVisited + 1, std::back_inserter(result));
std::copy(furthestPath + 1, path.end(), std::back_inserter(result));
return result;
}
// This function checks if the path has a small U-turn, that is,
// a polygon further in the path is adjacent to the first polygon
// in the path. If that happens, a shortcut is taken.
// This can happen if the target (T) location is at tile boundary,
// and we're (S) approaching it parallel to the tile edge.
// The choice at the vertex can be arbitrary,
// +---+---+
// |:::|:::|
// +-S-+-T-+
// |:::| | <-- the step can end up in here, resulting U-turn path.
// +---+---+
std::vector<dtPolyRef> fixupShortcuts(const std::vector<dtPolyRef>& path, const dtNavMeshQuery& navQuery)
{
if (path.size() < 3)
return path;
// Get connected polygons
const dtMeshTile* tile = 0;
const dtPoly* poly = 0;
if (dtStatusFailed(navQuery.getAttachedNavMesh()->getTileAndPolyByRef(path[0], &tile, &poly)))
return path;
const std::size_t maxNeis = 16;
std::array<dtPolyRef, maxNeis> neis;
std::size_t nneis = 0;
for (unsigned int k = poly->firstLink; k != DT_NULL_LINK; k = tile->links[k].next)
{
const dtLink* link = &tile->links[k];
if (link->ref != 0)
{
if (nneis < maxNeis)
neis[nneis++] = link->ref;
}
}
// If any of the neighbour polygons is within the next few polygons
// in the path, short cut to that polygon directly.
const std::size_t maxLookAhead = 6;
std::size_t cut = 0;
for (std::size_t i = std::min(maxLookAhead, path.size()) - 1; i > 1 && cut == 0; i--)
{
for (std::size_t j = 0; j < nneis; j++)
{
if (path[i] == neis[j])
{
cut = i;
break;
}
}
}
if (cut <= 1)
return path;
std::vector<dtPolyRef> result;
const auto offset = cut - 1;
result.reserve(1 + path.size() - offset);
result.push_back(path.front());
std::copy(path.begin() + std::ptrdiff_t(offset), path.end(), std::back_inserter(result));
return result;
}
boost::optional<SteerTarget> getSteerTarget(const dtNavMeshQuery& navQuery, const osg::Vec3f& startPos,
const osg::Vec3f& endPos, const float minTargetDist, const std::vector<dtPolyRef>& path)
{
// Find steer target.
SteerTarget result;
const int MAX_STEER_POINTS = 3;
std::array<float, MAX_STEER_POINTS * 3> steerPath;
std::array<unsigned char, MAX_STEER_POINTS> steerPathFlags;
std::array<dtPolyRef, MAX_STEER_POINTS> steerPathPolys;
int nsteerPath = 0;
navQuery.findStraightPath(startPos.ptr(), endPos.ptr(), path.data(), int(path.size()), steerPath.data(),
steerPathFlags.data(), steerPathPolys.data(), &nsteerPath, MAX_STEER_POINTS);
assert(nsteerPath >= 0);
if (!nsteerPath)
return boost::none;
// Find vertex far enough to steer to.
std::size_t ns = 0;
while (ns < static_cast<std::size_t>(nsteerPath))
{
// Stop at Off-Mesh link or when point is further than slop away.
if ((steerPathFlags[ns] & DT_STRAIGHTPATH_OFFMESH_CONNECTION) ||
!inRange(makeOsgVec3f(&steerPath[ns * 3]), startPos, minTargetDist, 1000.0f))
break;
ns++;
}
// Failed to find good point to steer to.
if (ns >= static_cast<std::size_t>(nsteerPath))
return boost::none;
dtVcopy(result.steerPos.ptr(), &steerPath[ns * 3]);
result.steerPos.y() = startPos[1];
result.steerPosFlag = steerPathFlags[ns];
result.steerPosRef = steerPathPolys[ns];
return result;
}
}

@ -0,0 +1,326 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_FINDSMOOTHPATH_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_FINDSMOOTHPATH_H
#include "dtstatus.hpp"
#include "exceptions.hpp"
#include "flags.hpp"
#include "settings.hpp"
#include "settingsutils.hpp"
#include "debug.hpp"
#include <DetourCommon.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshQuery.h>
#include <LinearMath/btVector3.h>
#include <boost/optional.hpp>
#include <osg/Vec3f>
#include <vector>
class dtNavMesh;
namespace DetourNavigator
{
struct Settings;
inline osg::Vec3f makeOsgVec3f(const float* values)
{
return osg::Vec3f(values[0], values[1], values[2]);
}
inline osg::Vec3f makeOsgVec3f(const btVector3& value)
{
return osg::Vec3f(value.x(), value.y(), value.z());
}
inline bool inRange(const osg::Vec3f& v1, const osg::Vec3f& v2, const float r, const float h)
{
const auto d = v2 - v1;
return (d.x() * d.x() + d.z() * d.z()) < r * r && std::abs(d.y()) < h;
}
std::vector<dtPolyRef> fixupCorridor(const std::vector<dtPolyRef>& path, const std::vector<dtPolyRef>& visited);
// This function checks if the path has a small U-turn, that is,
// a polygon further in the path is adjacent to the first polygon
// in the path. If that happens, a shortcut is taken.
// This can happen if the target (T) location is at tile boundary,
// and we're (S) approaching it parallel to the tile edge.
// The choice at the vertex can be arbitrary,
// +---+---+
// |:::|:::|
// +-S-+-T-+
// |:::| | <-- the step can end up in here, resulting U-turn path.
// +---+---+
std::vector<dtPolyRef> fixupShortcuts(const std::vector<dtPolyRef>& path, const dtNavMeshQuery& navQuery);
struct SteerTarget
{
osg::Vec3f steerPos;
unsigned char steerPosFlag;
dtPolyRef steerPosRef;
};
boost::optional<SteerTarget> getSteerTarget(const dtNavMeshQuery& navQuery, const osg::Vec3f& startPos,
const osg::Vec3f& endPos, const float minTargetDist, const std::vector<dtPolyRef>& path);
template <class OutputIterator>
class OutputTransformIterator
{
public:
OutputTransformIterator(OutputIterator& impl, const Settings& settings)
: mImpl(impl), mSettings(settings)
{
}
OutputTransformIterator& operator *()
{
return *this;
}
OutputTransformIterator& operator ++(int)
{
mImpl++;
return *this;
}
OutputTransformIterator& operator =(const osg::Vec3f& value)
{
*mImpl = fromNavMeshCoordinates(mSettings, value);
return *this;
}
private:
OutputIterator& mImpl;
const Settings& mSettings;
};
inline void initNavMeshQuery(dtNavMeshQuery& value, const dtNavMesh& navMesh, const int maxNodes)
{
const auto status = value.init(&navMesh, maxNodes);
if (!dtStatusSucceed(status))
throw NavigatorException("Failed to init navmesh query");
}
struct MoveAlongSurfaceResult
{
osg::Vec3f mResultPos;
std::vector<dtPolyRef> mVisited;
};
inline MoveAlongSurfaceResult moveAlongSurface(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef,
const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& filter,
const std::size_t maxVisitedSize)
{
MoveAlongSurfaceResult result;
result.mVisited.resize(maxVisitedSize);
int visitedNumber = 0;
const auto status = navMeshQuery.moveAlongSurface(startRef, startPos.ptr(), endPos.ptr(),
&filter, result.mResultPos.ptr(), result.mVisited.data(), &visitedNumber, static_cast<int>(maxVisitedSize));
if (!dtStatusSucceed(status))
{
std::ostringstream message;
message << "Failed to move along surface from " << startPos << " to " << endPos;
throw NavigatorException(message.str());
}
assert(visitedNumber >= 0);
assert(visitedNumber <= static_cast<int>(maxVisitedSize));
result.mVisited.resize(static_cast<std::size_t>(visitedNumber));
return result;
}
inline std::vector<dtPolyRef> findPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef,
const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter,
const std::size_t maxSize)
{
int pathLen = 0;
std::vector<dtPolyRef> result(maxSize);
const auto status = navMeshQuery.findPath(startRef, endRef, startPos.ptr(), endPos.ptr(), &queryFilter,
result.data(), &pathLen, static_cast<int>(maxSize));
if (!dtStatusSucceed(status))
{
std::ostringstream message;
message << "Failed to find path over polygons from " << startRef << " to " << endRef;
throw NavigatorException(message.str());
}
assert(pathLen >= 0);
assert(static_cast<std::size_t>(pathLen) <= maxSize);
result.resize(static_cast<std::size_t>(pathLen));
return result;
}
inline float getPolyHeight(const dtNavMeshQuery& navMeshQuery, const dtPolyRef ref, const osg::Vec3f& pos)
{
float result = 0.0f;
const auto status = navMeshQuery.getPolyHeight(ref, pos.ptr(), &result);
if (!dtStatusSucceed(status))
{
std::ostringstream message;
message << "Failed to get polygon height ref=" << ref << " pos=" << pos;
throw NavigatorException(message.str());
}
return result;
}
template <class OutputIterator>
OutputIterator makeSmoothPath(const dtNavMesh& navMesh, const dtNavMeshQuery& navMeshQuery,
const dtQueryFilter& filter, const osg::Vec3f& start, const osg::Vec3f& end,
std::vector<dtPolyRef> polygonPath, std::size_t maxSmoothPathSize, OutputIterator out)
{
// Iterate over the path to find smooth path on the detail mesh surface.
osg::Vec3f iterPos;
navMeshQuery.closestPointOnPoly(polygonPath.front(), start.ptr(), iterPos.ptr(), 0);
osg::Vec3f targetPos;
navMeshQuery.closestPointOnPoly(polygonPath.back(), end.ptr(), targetPos.ptr(), 0);
const float STEP_SIZE = 0.5f;
const float SLOP = 0.01f;
*out++ = iterPos;
std::size_t smoothPathSize = 1;
// Move towards target a small advancement at a time until target reached or
// when ran out of memory to store the path.
while (!polygonPath.empty() && smoothPathSize < maxSmoothPathSize)
{
// Find location to steer towards.
const auto steerTarget = getSteerTarget(navMeshQuery, iterPos, targetPos, SLOP, polygonPath);
if (!steerTarget)
break;
const bool endOfPath = bool(steerTarget->steerPosFlag & DT_STRAIGHTPATH_END);
const bool offMeshConnection = bool(steerTarget->steerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION);
// Find movement delta.
const osg::Vec3f delta = steerTarget->steerPos - iterPos;
float len = delta.length();
// If the steer target is end of path or off-mesh link, do not move past the location.
if ((endOfPath || offMeshConnection) && len < STEP_SIZE)
len = 1;
else
len = STEP_SIZE / len;
const osg::Vec3f moveTgt = iterPos + delta * len;
const auto result = moveAlongSurface(navMeshQuery, polygonPath.front(), iterPos, moveTgt, filter, 16);
polygonPath = fixupCorridor(polygonPath, result.mVisited);
polygonPath = fixupShortcuts(polygonPath, navMeshQuery);
float h = 0;
navMeshQuery.getPolyHeight(polygonPath.front(), result.mResultPos.ptr(), &h);
iterPos = result.mResultPos;
iterPos.y() = h;
// Handle end of path and off-mesh links when close enough.
if (endOfPath && inRange(iterPos, steerTarget->steerPos, SLOP, 1.0f))
{
// Reached end of path.
iterPos = targetPos;
*out++ = iterPos;
++smoothPathSize;
break;
}
else if (offMeshConnection && inRange(iterPos, steerTarget->steerPos, SLOP, 1.0f))
{
// Advance the path up to and over the off-mesh connection.
dtPolyRef prevRef = 0;
dtPolyRef polyRef = polygonPath.front();
std::size_t npos = 0;
while (npos < polygonPath.size() && polyRef != steerTarget->steerPosRef)
{
prevRef = polyRef;
polyRef = polygonPath[npos];
++npos;
}
std::copy(polygonPath.begin() + std::ptrdiff_t(npos), polygonPath.end(), polygonPath.begin());
polygonPath.resize(polygonPath.size() - npos);
// Reached off-mesh connection.
osg::Vec3f startPos;
osg::Vec3f endPos;
// Handle the connection.
if (dtStatusSucceed(navMesh.getOffMeshConnectionPolyEndPoints(prevRef, polyRef,
startPos.ptr(), endPos.ptr())))
{
*out++ = startPos;
++smoothPathSize;
// Hack to make the dotted path not visible during off-mesh connection.
if (smoothPathSize & 1)
{
*out++ = startPos;
++smoothPathSize;
}
// Move position at the other side of the off-mesh link.
iterPos = endPos;
iterPos.y() = getPolyHeight(navMeshQuery, polygonPath.front(), iterPos);
}
}
// Store results.
*out++ = iterPos;
++smoothPathSize;
}
return out;
}
template <class OutputIterator>
OutputIterator findSmoothPath(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
const osg::Vec3f& start, const osg::Vec3f& end, const Flags includeFlags,
const Settings& settings, OutputIterator out)
{
dtNavMeshQuery navMeshQuery;
initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes);
dtQueryFilter queryFilter;
queryFilter.setIncludeFlags(includeFlags);
dtPolyRef startRef = 0;
osg::Vec3f startPolygonPosition;
for (int i = 0; i < 3; ++i)
{
const auto status = navMeshQuery.findNearestPoly(start.ptr(), (halfExtents * (1 << i)).ptr(), &queryFilter,
&startRef, startPolygonPosition.ptr());
if (!dtStatusFailed(status) && startRef != 0)
break;
}
if (startRef == 0)
throw NavigatorException("Navmesh polygon for start point is not found");
dtPolyRef endRef = 0;
osg::Vec3f endPolygonPosition;
for (int i = 0; i < 3; ++i)
{
const auto status = navMeshQuery.findNearestPoly(end.ptr(), (halfExtents * (1 << i)).ptr(), &queryFilter,
&endRef, endPolygonPosition.ptr());
if (!dtStatusFailed(status) && endRef != 0)
break;
}
if (endRef == 0)
throw NavigatorException("Navmesh polygon for end polygon is not found");
const auto polygonPath = findPath(navMeshQuery, startRef, endRef, start, end, queryFilter,
settings.mMaxPolygonPathSize);
if (polygonPath.empty() || polygonPath.back() != endRef)
return out;
makeSmoothPath(navMesh, navMeshQuery, queryFilter, start, end, std::move(polygonPath),
settings.mMaxSmoothPathSize, OutputTransformIterator<OutputIterator>(out, settings));
return out;
}
}
#endif

@ -0,0 +1,62 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_FLAGS_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_FLAGS_H
#include <ostream>
namespace DetourNavigator
{
using Flags = unsigned short;
enum Flag : Flags
{
Flag_none = 0,
Flag_walk = 1 << 0,
Flag_swim = 1 << 1,
Flag_openDoor = 1 << 2,
};
inline std::ostream& operator <<(std::ostream& stream, const Flag value)
{
switch (value) {
case Flag_none:
return stream << "none";
case Flag_walk:
return stream << "walk";
case Flag_swim:
return stream << "swim";
case Flag_openDoor:
return stream << "openDoor";
}
}
struct WriteFlags
{
Flags mValue;
friend inline std::ostream& operator <<(std::ostream& stream, const WriteFlags& value)
{
if (value.mValue == Flag_none)
{
return stream << Flag_none;
}
else
{
bool first = true;
for (const auto flag : {Flag_walk, Flag_swim, Flag_openDoor})
{
if (value.mValue & flag)
{
if (!first)
stream << " | ";
first = false;
stream << flag;
}
}
return stream;
}
}
};
}
#endif

@ -0,0 +1,73 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_GETTILESPOSITIONS_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_GETTILESPOSITIONS_H
#include "settings.hpp"
#include "settingsutils.hpp"
#include "tileposition.hpp"
#include <BulletCollision/CollisionShapes/btCollisionShape.h>
#include <osg/Vec3f>
namespace DetourNavigator
{
inline osg::Vec3f makeOsgVec3f(const btVector3& value)
{
return osg::Vec3f(value.x(), value.y(), value.z());
}
template <class Callback>
void getTilesPositions(const osg::Vec3f& aabbMin, const osg::Vec3f& aabbMax,
const Settings& settings, Callback&& callback)
{
auto min = toNavMeshCoordinates(settings, aabbMin);
auto max = toNavMeshCoordinates(settings, aabbMax);
const auto border = getBorderSize(settings);
min -= osg::Vec3f(border, border, border);
max += osg::Vec3f(border, border, border);
auto minTile = getTilePosition(settings, min);
auto maxTile = getTilePosition(settings, max);
if (minTile.x() > maxTile.x())
std::swap(minTile.x(), maxTile.x());
if (minTile.y() > maxTile.y())
std::swap(minTile.y(), maxTile.y());
for (int tileX = minTile.x(); tileX <= maxTile.x(); ++tileX)
for (int tileY = minTile.y(); tileY <= maxTile.y(); ++tileY)
callback(TilePosition {tileX, tileY});
}
template <class Callback>
void getTilesPositions(const btCollisionShape& shape, const btTransform& transform,
const Settings& settings, Callback&& callback)
{
btVector3 aabbMin;
btVector3 aabbMax;
shape.getAabb(transform, aabbMin, aabbMax);
getTilesPositions(makeOsgVec3f(aabbMin), makeOsgVec3f(aabbMax), settings, std::forward<Callback>(callback));
}
template <class Callback>
void getTilesPositions(const int cellSize, const btTransform& transform,
const Settings& settings, Callback&& callback)
{
const auto halfCellSize = cellSize / 2;
auto aabbMin = transform(btVector3(-halfCellSize, -halfCellSize, 0));
auto aabbMax = transform(btVector3(halfCellSize, halfCellSize, 0));
aabbMin.setX(std::min(aabbMin.x(), aabbMax.x()));
aabbMin.setY(std::min(aabbMin.y(), aabbMax.y()));
aabbMax.setX(std::max(aabbMin.x(), aabbMax.x()));
aabbMax.setY(std::max(aabbMin.y(), aabbMax.y()));
getTilesPositions(makeOsgVec3f(aabbMin), makeOsgVec3f(aabbMax), settings, std::forward<Callback>(callback));
}
}
#endif

@ -0,0 +1,633 @@
#include "makenavmesh.hpp"
#include "chunkytrimesh.hpp"
#include "debug.hpp"
#include "dtstatus.hpp"
#include "exceptions.hpp"
#include "recastmesh.hpp"
#include "settings.hpp"
#include "settingsutils.hpp"
#include "sharednavmesh.hpp"
#include "settingsutils.hpp"
#include "flags.hpp"
#include "navmeshtilescache.hpp"
#include <DetourNavMesh.h>
#include <DetourNavMeshBuilder.h>
#include <Recast.h>
#include <RecastAlloc.h>
#include <algorithm>
#include <iomanip>
#include <limits>
namespace
{
using namespace DetourNavigator;
static const int doNotTransferOwnership = 0;
void initPolyMeshDetail(rcPolyMeshDetail& value)
{
value.meshes = nullptr;
value.verts = nullptr;
value.tris = nullptr;
}
struct PolyMeshDetailStackDeleter
{
void operator ()(rcPolyMeshDetail* value) const
{
rcFree(value->meshes);
rcFree(value->verts);
rcFree(value->tris);
}
};
using PolyMeshDetailStackPtr = std::unique_ptr<rcPolyMeshDetail, PolyMeshDetailStackDeleter>;
osg::Vec3f makeOsgVec3f(const btVector3& value)
{
return osg::Vec3f(value.x(), value.y(), value.z());
}
struct WaterBounds
{
osg::Vec3f mMin;
osg::Vec3f mMax;
};
WaterBounds getWaterBounds(const RecastMesh::Water& water, const Settings& settings,
const osg::Vec3f& agentHalfExtents)
{
if (water.mCellSize == std::numeric_limits<int>::max())
{
const auto transform = getSwimLevelTransform(settings, water.mTransform, agentHalfExtents.z());
const auto min = toNavMeshCoordinates(settings, makeOsgVec3f(transform(btVector3(-1, -1, 0))));
const auto max = toNavMeshCoordinates(settings, makeOsgVec3f(transform(btVector3(1, 1, 0))));
return WaterBounds {
osg::Vec3f(-std::numeric_limits<float>::max(), min.y(), -std::numeric_limits<float>::max()),
osg::Vec3f(std::numeric_limits<float>::max(), max.y(), std::numeric_limits<float>::max())
};
}
else
{
const auto transform = getSwimLevelTransform(settings, water.mTransform, agentHalfExtents.z());
const auto halfCellSize = water.mCellSize / 2.0f;
return WaterBounds {
toNavMeshCoordinates(settings, makeOsgVec3f(transform(btVector3(-halfCellSize, -halfCellSize, 0)))),
toNavMeshCoordinates(settings, makeOsgVec3f(transform(btVector3(halfCellSize, halfCellSize, 0))))
};
}
}
std::vector<float> getOffMeshVerts(const std::vector<OffMeshConnection>& connections)
{
std::vector<float> result;
result.reserve(connections.size() * 6);
const auto add = [&] (const osg::Vec3f& v)
{
result.push_back(v.x());
result.push_back(v.y());
result.push_back(v.z());
};
for (const auto& v : connections)
{
add(v.mStart);
add(v.mEnd);
}
return result;
}
rcConfig makeConfig(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax,
const Settings& settings)
{
rcConfig config;
config.cs = settings.mCellSize;
config.ch = settings.mCellHeight;
config.walkableSlopeAngle = settings.mMaxSlope;
config.walkableHeight = static_cast<int>(std::ceil(getHeight(settings, agentHalfExtents) / config.ch));
config.walkableClimb = static_cast<int>(std::floor(getMaxClimb(settings) / config.ch));
config.walkableRadius = static_cast<int>(std::ceil(getRadius(settings, agentHalfExtents) / config.cs));
config.maxEdgeLen = static_cast<int>(std::round(settings.mMaxEdgeLen / config.cs));
config.maxSimplificationError = settings.mMaxSimplificationError;
config.minRegionArea = settings.mRegionMinSize * settings.mRegionMinSize;
config.mergeRegionArea = settings.mRegionMergeSize * settings.mRegionMergeSize;
config.maxVertsPerPoly = settings.mMaxVertsPerPoly;
config.detailSampleDist = settings.mDetailSampleDist < 0.9f ? 0 : config.cs * settings.mDetailSampleDist;
config.detailSampleMaxError = config.ch * settings.mDetailSampleMaxError;
config.borderSize = settings.mBorderSize;
config.width = settings.mTileSize + config.borderSize * 2;
config.height = settings.mTileSize + config.borderSize * 2;
rcVcopy(config.bmin, boundsMin.ptr());
rcVcopy(config.bmax, boundsMax.ptr());
config.bmin[0] -= getBorderSize(settings);
config.bmin[2] -= getBorderSize(settings);
config.bmax[0] += getBorderSize(settings);
config.bmax[2] += getBorderSize(settings);
return config;
}
void createHeightfield(rcContext& context, rcHeightfield& solid, int width, int height, const float* bmin,
const float* bmax, const float cs, const float ch)
{
const auto result = rcCreateHeightfield(&context, solid, width, height, bmin, bmax, cs, ch);
if (!result)
throw NavigatorException("Failed to create heightfield for navmesh");
}
bool rasterizeSolidObjectsTriangles(rcContext& context, const RecastMesh& recastMesh, const rcConfig& config,
rcHeightfield& solid)
{
const auto& chunkyMesh = recastMesh.getChunkyTriMesh();
std::vector<unsigned char> areas(chunkyMesh.getMaxTrisPerChunk(), AreaType_null);
const osg::Vec2f tileBoundsMin(config.bmin[0], config.bmin[2]);
const osg::Vec2f tileBoundsMax(config.bmax[0], config.bmax[2]);
std::vector<std::size_t> cids;
chunkyMesh.getChunksOverlappingRect(Rect {tileBoundsMin, tileBoundsMax}, std::back_inserter(cids));
if (cids.empty())
return false;
for (const auto cid : cids)
{
const auto chunk = chunkyMesh.getChunk(cid);
std::fill(
areas.begin(),
std::min(areas.begin() + static_cast<std::ptrdiff_t>(chunk.mSize),
areas.end()),
AreaType_null
);
rcMarkWalkableTriangles(
&context,
config.walkableSlopeAngle,
recastMesh.getVertices().data(),
static_cast<int>(recastMesh.getVerticesCount()),
chunk.mIndices,
static_cast<int>(chunk.mSize),
areas.data()
);
for (std::size_t i = 0; i < chunk.mSize; ++i)
areas[i] = chunk.mAreaTypes[i];
rcClearUnwalkableTriangles(
&context,
config.walkableSlopeAngle,
recastMesh.getVertices().data(),
static_cast<int>(recastMesh.getVerticesCount()),
chunk.mIndices,
static_cast<int>(chunk.mSize),
areas.data()
);
const auto trianglesRasterized = rcRasterizeTriangles(
&context,
recastMesh.getVertices().data(),
static_cast<int>(recastMesh.getVerticesCount()),
chunk.mIndices,
areas.data(),
static_cast<int>(chunk.mSize),
solid,
config.walkableClimb
);
if (!trianglesRasterized)
throw NavigatorException("Failed to create rasterize triangles from recast mesh for navmesh");
}
return true;
}
void rasterizeWaterTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh,
const Settings& settings, const rcConfig& config, rcHeightfield& solid)
{
const std::array<unsigned char, 2> areas {{AreaType_water, AreaType_water}};
for (const auto& water : recastMesh.getWater())
{
const auto bounds = getWaterBounds(water, settings, agentHalfExtents);
const osg::Vec2f tileBoundsMin(
std::min(config.bmax[0], std::max(config.bmin[0], bounds.mMin.x())),
std::min(config.bmax[2], std::max(config.bmin[2], bounds.mMin.z()))
);
const osg::Vec2f tileBoundsMax(
std::min(config.bmax[0], std::max(config.bmin[0], bounds.mMax.x())),
std::min(config.bmax[2], std::max(config.bmin[2], bounds.mMax.z()))
);
if (tileBoundsMax == tileBoundsMin)
continue;
const std::array<osg::Vec3f, 4> vertices {{
osg::Vec3f(tileBoundsMin.x(), bounds.mMin.y(), tileBoundsMin.y()),
osg::Vec3f(tileBoundsMin.x(), bounds.mMin.y(), tileBoundsMax.y()),
osg::Vec3f(tileBoundsMax.x(), bounds.mMin.y(), tileBoundsMax.y()),
osg::Vec3f(tileBoundsMax.x(), bounds.mMin.y(), tileBoundsMin.y()),
}};
std::array<float, 4 * 3> convertedVertices;
auto convertedVerticesIt = convertedVertices.begin();
for (const auto& vertex : vertices)
convertedVerticesIt = std::copy(vertex.ptr(), vertex.ptr() + 3, convertedVerticesIt);
const std::array<int, 6> indices {{
0, 1, 2,
0, 2, 3,
}};
const auto trianglesRasterized = rcRasterizeTriangles(
&context,
convertedVertices.data(),
static_cast<int>(convertedVertices.size() / 3),
indices.data(),
areas.data(),
static_cast<int>(areas.size()),
solid,
config.walkableClimb
);
if (!trianglesRasterized)
throw NavigatorException("Failed to create rasterize water triangles for navmesh");
}
}
bool rasterizeTriangles(rcContext& context, const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh,
const rcConfig& config, const Settings& settings, rcHeightfield& solid)
{
if (!rasterizeSolidObjectsTriangles(context, recastMesh, config, solid))
return false;
rasterizeWaterTriangles(context, agentHalfExtents, recastMesh, settings, config, solid);
return true;
}
void buildCompactHeightfield(rcContext& context, const int walkableHeight, const int walkableClimb,
rcHeightfield& solid, rcCompactHeightfield& compact)
{
const auto result = rcBuildCompactHeightfield(&context, walkableHeight,
walkableClimb, solid, compact);
if (!result)
throw NavigatorException("Failed to build compact heightfield for navmesh");
}
void erodeWalkableArea(rcContext& context, int walkableRadius, rcCompactHeightfield& compact)
{
const auto result = rcErodeWalkableArea(&context, walkableRadius, compact);
if (!result)
throw NavigatorException("Failed to erode walkable area for navmesh");
}
void buildDistanceField(rcContext& context, rcCompactHeightfield& compact)
{
const auto result = rcBuildDistanceField(&context, compact);
if (!result)
throw NavigatorException("Failed to build distance field for navmesh");
}
void buildRegions(rcContext& context, rcCompactHeightfield& compact, const int borderSize,
const int minRegionArea, const int mergeRegionArea)
{
const auto result = rcBuildRegions(&context, compact, borderSize, minRegionArea, mergeRegionArea);
if (!result)
throw NavigatorException("Failed to build distance field for navmesh");
}
void buildContours(rcContext& context, rcCompactHeightfield& compact, const float maxError, const int maxEdgeLen,
rcContourSet& contourSet, const int buildFlags = RC_CONTOUR_TESS_WALL_EDGES)
{
const auto result = rcBuildContours(&context, compact, maxError, maxEdgeLen, contourSet, buildFlags);
if (!result)
throw NavigatorException("Failed to build contours for navmesh");
}
void buildPolyMesh(rcContext& context, rcContourSet& contourSet, const int maxVertsPerPoly, rcPolyMesh& polyMesh)
{
const auto result = rcBuildPolyMesh(&context, contourSet, maxVertsPerPoly, polyMesh);
if (!result)
throw NavigatorException("Failed to build poly mesh for navmesh");
}
void buildPolyMeshDetail(rcContext& context, const rcPolyMesh& polyMesh, const rcCompactHeightfield& compact,
const float sampleDist, const float sampleMaxError, rcPolyMeshDetail& polyMeshDetail)
{
const auto result = rcBuildPolyMeshDetail(&context, polyMesh, compact, sampleDist, sampleMaxError,
polyMeshDetail);
if (!result)
throw NavigatorException("Failed to build detail poly mesh for navmesh");
}
void setPolyMeshFlags(rcPolyMesh& polyMesh)
{
for (int i = 0; i < polyMesh.npolys; ++i)
{
if (polyMesh.areas[i] == AreaType_ground)
polyMesh.flags[i] = Flag_walk;
else if (polyMesh.areas[i] == AreaType_water)
polyMesh.flags[i] = Flag_swim;
}
}
bool fillPolyMesh(rcContext& context, const rcConfig& config, rcHeightfield& solid, rcPolyMesh& polyMesh,
rcPolyMeshDetail& polyMeshDetail)
{
rcCompactHeightfield compact;
buildCompactHeightfield(context, config.walkableHeight, config.walkableClimb, solid, compact);
erodeWalkableArea(context, config.walkableRadius, compact);
buildDistanceField(context, compact);
buildRegions(context, compact, config.borderSize, config.minRegionArea, config.mergeRegionArea);
rcContourSet contourSet;
buildContours(context, compact, config.maxSimplificationError, config.maxEdgeLen, contourSet);
if (contourSet.nconts == 0)
return false;
buildPolyMesh(context, contourSet, config.maxVertsPerPoly, polyMesh);
buildPolyMeshDetail(context, polyMesh, compact, config.detailSampleDist, config.detailSampleMaxError,
polyMeshDetail);
setPolyMeshFlags(polyMesh);
return true;
}
NavMeshData makeNavMeshTileData(const osg::Vec3f& agentHalfExtents, const RecastMesh& recastMesh,
const std::vector<OffMeshConnection>& offMeshConnections, const TilePosition& tile,
const osg::Vec3f& boundsMin, const osg::Vec3f& boundsMax, const Settings& settings)
{
rcContext context;
const auto config = makeConfig(agentHalfExtents, boundsMin, boundsMax, settings);
rcHeightfield solid;
createHeightfield(context, solid, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch);
if (!rasterizeTriangles(context, agentHalfExtents, recastMesh, config, settings, solid))
return NavMeshData();
rcFilterLowHangingWalkableObstacles(&context, config.walkableClimb, solid);
rcFilterLedgeSpans(&context, config.walkableHeight, config.walkableClimb, solid);
rcFilterWalkableLowHeightSpans(&context, config.walkableHeight, solid);
rcPolyMesh polyMesh;
rcPolyMeshDetail polyMeshDetail;
initPolyMeshDetail(polyMeshDetail);
const PolyMeshDetailStackPtr polyMeshDetailPtr(&polyMeshDetail);
if (!fillPolyMesh(context, config, solid, polyMesh, polyMeshDetail))
return NavMeshData();
const auto offMeshConVerts = getOffMeshVerts(offMeshConnections);
const std::vector<float> offMeshConRad(offMeshConnections.size(), getRadius(settings, agentHalfExtents));
const std::vector<unsigned char> offMeshConDir(offMeshConnections.size(), DT_OFFMESH_CON_BIDIR);
const std::vector<unsigned char> offMeshConAreas(offMeshConnections.size(), AreaType_ground);
const std::vector<unsigned short> offMeshConFlags(offMeshConnections.size(), Flag_openDoor);
dtNavMeshCreateParams params;
params.verts = polyMesh.verts;
params.vertCount = polyMesh.nverts;
params.polys = polyMesh.polys;
params.polyAreas = polyMesh.areas;
params.polyFlags = polyMesh.flags;
params.polyCount = polyMesh.npolys;
params.nvp = polyMesh.nvp;
params.detailMeshes = polyMeshDetail.meshes;
params.detailVerts = polyMeshDetail.verts;
params.detailVertsCount = polyMeshDetail.nverts;
params.detailTris = polyMeshDetail.tris;
params.detailTriCount = polyMeshDetail.ntris;
params.offMeshConVerts = offMeshConVerts.data();
params.offMeshConRad = offMeshConRad.data();
params.offMeshConDir = offMeshConDir.data();
params.offMeshConAreas = offMeshConAreas.data();
params.offMeshConFlags = offMeshConFlags.data();
params.offMeshConUserID = nullptr;
params.offMeshConCount = static_cast<int>(offMeshConnections.size());
params.walkableHeight = getHeight(settings, agentHalfExtents);
params.walkableRadius = getRadius(settings, agentHalfExtents);
params.walkableClimb = getMaxClimb(settings);
rcVcopy(params.bmin, polyMesh.bmin);
rcVcopy(params.bmax, polyMesh.bmax);
params.cs = config.cs;
params.ch = config.ch;
params.buildBvTree = true;
params.userId = 0;
params.tileX = tile.x();
params.tileY = tile.y();
params.tileLayer = 0;
unsigned char* navMeshData;
int navMeshDataSize;
const auto navMeshDataCreated = dtCreateNavMeshData(&params, &navMeshData, &navMeshDataSize);
if (!navMeshDataCreated)
throw NavigatorException("Failed to create navmesh tile data");
return NavMeshData(navMeshData, navMeshDataSize);
}
UpdateNavMeshStatus makeUpdateNavMeshStatus(bool removed, bool add)
{
if (removed && add)
return UpdateNavMeshStatus::replaced;
else if (removed)
return UpdateNavMeshStatus::removed;
else if (add)
return UpdateNavMeshStatus::add;
else
return UpdateNavMeshStatus::ignore;
}
template <class T>
unsigned long getMinValuableBitsNumber(const T value)
{
unsigned long power = 0;
while (power < sizeof(T) * 8 && (static_cast<T>(1) << power) < value)
++power;
return power;
}
}
namespace DetourNavigator
{
NavMeshPtr makeEmptyNavMesh(const Settings& settings)
{
// Max tiles and max polys affect how the tile IDs are caculated.
// There are 22 bits available for identifying a tile and a polygon.
const int polysAndTilesBits = 22;
const auto polysBits = getMinValuableBitsNumber(settings.mMaxPolys);
if (polysBits >= polysAndTilesBits)
throw InvalidArgument("Too many polygons per tile");
const auto tilesBits = polysAndTilesBits - polysBits;
dtNavMeshParams params;
std::fill_n(params.orig, 3, 0.0f);
params.tileWidth = settings.mTileSize * settings.mCellSize;
params.tileHeight = settings.mTileSize * settings.mCellSize;
params.maxTiles = 1 << tilesBits;
params.maxPolys = 1 << polysBits;
NavMeshPtr navMesh(dtAllocNavMesh(), &dtFreeNavMesh);
const auto status = navMesh->init(&params);
if (!dtStatusSucceed(status))
throw NavigatorException("Failed to init navmesh");
return navMesh;
}
UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh,
const TilePosition& changedTile, const TilePosition& playerTile,
const std::vector<OffMeshConnection>& offMeshConnections, const Settings& settings,
const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache)
{
log("update NavMesh with mutiple tiles:",
" agentHeight=", std::setprecision(std::numeric_limits<float>::max_exponent10),
getHeight(settings, agentHalfExtents),
" agentMaxClimb=", std::setprecision(std::numeric_limits<float>::max_exponent10),
getMaxClimb(settings),
" agentRadius=", std::setprecision(std::numeric_limits<float>::max_exponent10),
getRadius(settings, agentHalfExtents),
" changedTile=", changedTile,
" playerTile=", playerTile,
" changedTileDistance=", getDistance(changedTile, playerTile));
const auto params = *navMeshCacheItem.lockConst()->getValue().getParams();
const osg::Vec3f origin(params.orig[0], params.orig[1], params.orig[2]);
const auto x = changedTile.x();
const auto y = changedTile.y();
const auto removeTile = [&] {
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));
if (removed)
locked->removeUsedTile(changedTile);
return makeUpdateNavMeshStatus(removed, false);
};
if (!recastMesh)
{
log("ignore add tile: recastMesh is null");
return removeTile();
}
auto recastMeshBounds = recastMesh->getBounds();
for (const auto& water : recastMesh->getWater())
{
const auto waterBounds = getWaterBounds(water, settings, agentHalfExtents);
recastMeshBounds.mMin.y() = std::min(recastMeshBounds.mMin.y(), waterBounds.mMin.y());
recastMeshBounds.mMax.y() = std::max(recastMeshBounds.mMax.y(), waterBounds.mMax.y());
}
if (isEmpty(recastMeshBounds))
{
log("ignore add tile: recastMesh is empty");
return removeTile();
}
if (!shouldAddTile(changedTile, playerTile, params.maxTiles))
{
log("ignore add tile: too far from player");
return removeTile();
}
auto cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh, offMeshConnections);
if (!cachedNavMeshData)
{
const auto tileBounds = makeTileBounds(settings, changedTile);
const osg::Vec3f tileBorderMin(tileBounds.mMin.x(), recastMeshBounds.mMin.y() - 1, tileBounds.mMin.y());
const osg::Vec3f tileBorderMax(tileBounds.mMax.x(), recastMeshBounds.mMax.y() + 1, tileBounds.mMax.y());
auto navMeshData = makeNavMeshTileData(agentHalfExtents, *recastMesh, offMeshConnections, changedTile,
tileBorderMin, tileBorderMax, settings);
if (!navMeshData.mValue)
{
log("ignore add tile: NavMeshData is null");
return removeTile();
}
try
{
cachedNavMeshData = navMeshTilesCache.set(agentHalfExtents, changedTile, *recastMesh,
offMeshConnections, std::move(navMeshData));
}
catch (const InvalidArgument&)
{
cachedNavMeshData = navMeshTilesCache.get(agentHalfExtents, changedTile, *recastMesh,
offMeshConnections);
}
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);
}
}
}
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);
}
}
}

@ -0,0 +1,55 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_MAKENAVMESH_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_MAKENAVMESH_H
#include "offmeshconnectionsmanager.hpp"
#include "settings.hpp"
#include "navmeshcacheitem.hpp"
#include "tileposition.hpp"
#include "tilebounds.hpp"
#include "sharednavmesh.hpp"
#include "navmeshtilescache.hpp"
#include <osg/Vec3f>
#include <memory>
class dtNavMesh;
namespace DetourNavigator
{
class RecastMesh;
struct Settings;
enum class UpdateNavMeshStatus
{
ignore,
removed,
add,
replaced
};
inline float getLength(const osg::Vec2i& value)
{
return std::sqrt(float(osg::square(value.x()) + osg::square(value.y())));
}
inline float getDistance(const TilePosition& lhs, const TilePosition& rhs)
{
return getLength(lhs - rhs);
}
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;
}
NavMeshPtr makeEmptyNavMesh(const Settings& settings);
UpdateNavMeshStatus updateNavMesh(const osg::Vec3f& agentHalfExtents, const RecastMesh* recastMesh,
const TilePosition& changedTile, const TilePosition& playerTile,
const std::vector<OffMeshConnection>& offMeshConnections, const Settings& settings,
const SharedNavMeshCacheItem& navMeshCacheItem, NavMeshTilesCache& navMeshTilesCache);
}
#endif

@ -0,0 +1,154 @@
#include "navigator.hpp"
#include "debug.hpp"
#include "settingsutils.hpp"
#include <Recast.h>
namespace DetourNavigator
{
Navigator::Navigator(const Settings& settings)
: mSettings(settings)
, mNavMeshManager(mSettings)
{
}
void Navigator::addAgent(const osg::Vec3f& agentHalfExtents)
{
++mAgents[agentHalfExtents];
mNavMeshManager.addAgent(agentHalfExtents);
}
void Navigator::removeAgent(const osg::Vec3f& agentHalfExtents)
{
const auto it = mAgents.find(agentHalfExtents);
if (it == mAgents.end() || --it->second)
return;
mAgents.erase(it);
mNavMeshManager.reset(agentHalfExtents);
}
bool Navigator::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform)
{
return mNavMeshManager.addObject(id, shape, transform, AreaType_ground);
}
bool Navigator::addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform)
{
bool result = addObject(id, shapes.mShape, transform);
if (shapes.mAvoid)
{
const ObjectId avoidId(shapes.mAvoid);
if (mNavMeshManager.addObject(avoidId, *shapes.mAvoid, transform, AreaType_null))
{
updateAvoidShapeId(id, avoidId);
result = true;
}
}
return result;
}
bool Navigator::addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform)
{
if (addObject(id, static_cast<const ObjectShapes&>(shapes), transform))
{
mNavMeshManager.addOffMeshConnection(
id,
toNavMeshCoordinates(mSettings, shapes.mConnectionStart),
toNavMeshCoordinates(mSettings, shapes.mConnectionEnd)
);
return true;
}
return false;
}
bool Navigator::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform)
{
return mNavMeshManager.updateObject(id, shape, transform, AreaType_ground);
}
bool Navigator::updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform)
{
bool result = updateObject(id, shapes.mShape, transform);
if (shapes.mAvoid)
{
const ObjectId avoidId(shapes.mAvoid);
if (mNavMeshManager.updateObject(avoidId, *shapes.mAvoid, transform, AreaType_null))
{
updateAvoidShapeId(id, avoidId);
result = true;
}
}
return result;
}
bool Navigator::updateObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform)
{
return updateObject(id, static_cast<const ObjectShapes&>(shapes), transform);
}
bool Navigator::removeObject(const ObjectId id)
{
bool result = mNavMeshManager.removeObject(id);
const auto avoid = mAvoidIds.find(id);
if (avoid != mAvoidIds.end())
result = mNavMeshManager.removeObject(avoid->second) || result;
const auto water = mWaterIds.find(id);
if (water != mWaterIds.end())
result = mNavMeshManager.removeObject(water->second) || result;
mNavMeshManager.removeOffMeshConnection(id);
return result;
}
bool Navigator::addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level,
const btTransform& transform)
{
return mNavMeshManager.addWater(cellPosition, cellSize,
btTransform(transform.getBasis(), btVector3(transform.getOrigin().x(), transform.getOrigin().y(), level)));
}
bool Navigator::removeWater(const osg::Vec2i& cellPosition)
{
return mNavMeshManager.removeWater(cellPosition);
}
void Navigator::update(const osg::Vec3f& playerPosition)
{
for (const auto& v : mAgents)
mNavMeshManager.update(playerPosition, v.first);
}
void Navigator::wait()
{
mNavMeshManager.wait();
}
std::map<osg::Vec3f, SharedNavMeshCacheItem> Navigator::getNavMeshes() const
{
return mNavMeshManager.getNavMeshes();
}
const Settings& Navigator::getSettings() const
{
return mSettings;
}
void Navigator::updateAvoidShapeId(const ObjectId id, const ObjectId avoidId)
{
updateId(id, avoidId, mWaterIds);
}
void Navigator::updateWaterShapeId(const ObjectId id, const ObjectId waterId)
{
updateId(id, waterId, mWaterIds);
}
void Navigator::updateId(const ObjectId id, const ObjectId updateId, std::unordered_map<ObjectId, ObjectId>& ids)
{
auto inserted = ids.insert(std::make_pair(id, updateId));
if (!inserted.second)
{
mNavMeshManager.removeObject(inserted.first->second);
inserted.first->second = updateId;
}
}
}

@ -0,0 +1,205 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVIGATOR_H
#include "findsmoothpath.hpp"
#include "flags.hpp"
#include "navmeshmanager.hpp"
#include "settings.hpp"
#include "settingsutils.hpp"
namespace DetourNavigator
{
struct ObjectShapes
{
const btCollisionShape& mShape;
const btCollisionShape* mAvoid;
ObjectShapes(const btCollisionShape& shape, const btCollisionShape* avoid)
: mShape(shape), mAvoid(avoid)
{}
};
struct DoorShapes : ObjectShapes
{
osg::Vec3f mConnectionStart;
osg::Vec3f mConnectionEnd;
DoorShapes(const btCollisionShape& shape, const btCollisionShape* avoid,
const osg::Vec3f& connectionStart,const osg::Vec3f& connectionEnd)
: ObjectShapes(shape, avoid)
, mConnectionStart(connectionStart)
, mConnectionEnd(connectionEnd)
{}
};
/**
* @brief Top level class of detournavigator componenet. Navigator allows to build a scene with navmesh and find
* a path for an agent there. Scene contains agents, geometry objects and water. Agent are distinguished only by
* half extents. Each object has unique identifier and could be added, updated or removed. Water could be added once
* for each world cell at given level of height. Navmesh builds asynchronously in separate threads. To start build
* navmesh call update method.
*/
class Navigator
{
public:
/**
* @brief Navigator constructor initializes all internal data. Constructed object is ready to build a scene.
* @param settings allows to customize navigator work. Constructor is only place to set navigator settings.
*/
Navigator(const Settings& settings);
/**
* @brief addAgent should be called for each agent even if all of them has same half extents.
* @param agentHalfExtents allows to setup bounding cylinder for each agent, for each different half extents
* there is different navmesh.
*/
void addAgent(const osg::Vec3f& agentHalfExtents);
/**
* @brief removeAgent should be called for each agent even if all of them has same half extents
* @param agentHalfExtents allows determine which agent to remove
*/
void removeAgent(const osg::Vec3f& agentHalfExtents);
/**
* @brief addObject is used to add object represented by single btCollisionShape and btTransform.
* @param id is used to distinguish different objects.
* @param shape must live until object is updated by another shape removed from Navigator.
* @param transform allows to setup object geometry according to its world state.
* @return true if object is added, false if there is already object with given id.
*/
bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform);
/**
* @brief addObject is used to add complex object with allowed to walk and avoided to walk shapes
* @param id is used to distinguish different objects
* @param shape members must live until object is updated by another shape removed from Navigator
* @param transform allows to setup objects geometry according to its world state
* @return true if object is added, false if there is already object with given id
*/
bool addObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform);
/**
* @brief addObject is used to add doors.
* @param id is used to distinguish different objects.
* @param shape members must live until object is updated by another shape or removed from Navigator.
* @param transform allows to setup objects geometry according to its world state.
* @return true if object is added, false if there is already object with given id.
*/
bool addObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform);
/**
* @brief updateObject replace object geometry by given data.
* @param id is used to find object.
* @param shape must live until object is updated by another shape removed from Navigator.
* @param transform allows to setup objects geometry according to its world state.
* @return true if object is updated, false if there is no object with given id.
*/
bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform);
/**
* @brief updateObject replace object geometry by given data.
* @param id is used to find object.
* @param shape members must live until object is updated by another shape removed from Navigator.
* @param transform allows to setup objects geometry according to its world state.
* @return true if object is updated, false if there is no object with given id.
*/
bool updateObject(const ObjectId id, const ObjectShapes& shapes, const btTransform& transform);
/**
* @brief updateObject replace object geometry by given data.
* @param id is used to find object.
* @param shape members must live until object is updated by another shape removed from Navigator.
* @param transform allows to setup objects geometry according to its world state.
* @return true if object is updated, false if there is no object with given id.
*/
bool updateObject(const ObjectId id, const DoorShapes& shapes, const btTransform& transform);
/**
* @brief removeObject to make it no more available at the scene.
* @param id is used to find object.
* @return true if object is removed, false if there is no object with given id.
*/
bool removeObject(const ObjectId id);
/**
* @brief addWater is used to set water level at given world cell.
* @param cellPosition allows to distinguish cells if there is many in current world.
* @param cellSize set cell borders. std::numeric_limits<int>::max() disables cell borders.
* @param level set z coordinate of water surface at the scene.
* @param transform set global shift of cell center.
* @return true if there was no water at given cell if cellSize != std::numeric_limits<int>::max() or there is
* at least single object is added to the scene, false if there is already water for given cell or there is no
* any other objects.
*/
bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btScalar level,
const btTransform& transform);
/**
* @brief removeWater to make it no more available at the scene.
* @param cellPosition allows to find cell.
* @return true if there was water at given cell.
*/
bool removeWater(const osg::Vec2i& cellPosition);
/**
* @brief update start background navmesh update using current scene state.
* @param playerPosition setup initial point to order build tiles of navmesh.
*/
void update(const osg::Vec3f& playerPosition);
/**
* @brief wait locks thread until all tiles are updated from last update call.
*/
void wait();
/**
* @brief findPath fills output iterator with points of scene surfaces to be used for actor to walk through.
* @param agentHalfExtents allows to find navmesh for given actor.
* @param start path from given point.
* @param end path at given point.
* @param includeFlags setup allowed surfaces for actor to walk.
* @param out the beginning of the destination range.
* @return Output iterator to the element in the destination range, one past the last element of found path.
* Equal to out if no path is found.
* @throws InvalidArgument if there is no navmesh for given agentHalfExtents.
*/
template <class OutputIterator>
OutputIterator findPath(const osg::Vec3f& agentHalfExtents, const osg::Vec3f& start,
const osg::Vec3f& end, const Flags includeFlags, OutputIterator out) const
{
static_assert(
std::is_same<
typename std::iterator_traits<OutputIterator>::iterator_category,
std::output_iterator_tag
>::value,
"out is not an OutputIterator"
);
const auto navMesh = mNavMeshManager.getNavMesh(agentHalfExtents);
return findSmoothPath(navMesh.lock()->getValue(), toNavMeshCoordinates(mSettings, agentHalfExtents),
toNavMeshCoordinates(mSettings, start), toNavMeshCoordinates(mSettings, end), includeFlags,
mSettings, out);
}
/**
* @brief getNavMeshes returns all current navmeshes
* @return map of agent half extents to navmesh
*/
std::map<osg::Vec3f, SharedNavMeshCacheItem> getNavMeshes() const;
const Settings& getSettings() const;
private:
Settings mSettings;
NavMeshManager mNavMeshManager;
std::map<osg::Vec3f, std::size_t> mAgents;
std::unordered_map<ObjectId, ObjectId> mAvoidIds;
std::unordered_map<ObjectId, ObjectId> mWaterIds;
void updateAvoidShapeId(const ObjectId id, const ObjectId avoidId);
void updateWaterShapeId(const ObjectId id, const ObjectId waterId);
void updateId(const ObjectId id, const ObjectId waterId, std::unordered_map<ObjectId, ObjectId>& ids);
};
}
#endif

@ -0,0 +1,70 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHCACHEITEM_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHCACHEITEM_H
#include "sharednavmesh.hpp"
#include "tileposition.hpp"
#include "navmeshtilescache.hpp"
#include <components/misc/guarded.hpp>
#include <map>
namespace DetourNavigator
{
class NavMeshCacheItem
{
public:
NavMeshCacheItem(const NavMeshPtr& value, std::size_t generation)
: mValue(value), mGeneration(generation), mNavMeshRevision(0)
{
}
const dtNavMesh& getValue() const
{
return *mValue;
}
dtNavMesh& getValue()
{
return *mValue;
}
std::size_t getGeneration() const
{
return mGeneration;
}
std::size_t getNavMeshRevision() const
{
return mNavMeshRevision;
}
void setUsedTile(const TilePosition& tilePosition, NavMeshTilesCache::Value value)
{
mUsedTiles[tilePosition] = std::make_pair(std::move(value), NavMeshData());
++mNavMeshRevision;
}
void setUsedTile(const TilePosition& tilePosition, NavMeshData value)
{
mUsedTiles[tilePosition] = std::make_pair(NavMeshTilesCache::Value(), std::move(value));
++mNavMeshRevision;
}
void removeUsedTile(const TilePosition& tilePosition)
{
mUsedTiles.erase(tilePosition);
++mNavMeshRevision;
}
private:
NavMeshPtr mValue;
std::size_t mGeneration;
std::size_t mNavMeshRevision;
std::map<TilePosition, std::pair<NavMeshTilesCache::Value, NavMeshData>> mUsedTiles;
};
using SharedNavMeshCacheItem = Misc::SharedGuarded<NavMeshCacheItem>;
}
#endif

@ -0,0 +1,35 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDATA_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHDATA_H
#include <DetourAlloc.h>
#include <algorithm>
#include <memory>
namespace DetourNavigator
{
struct NavMeshDataValueDeleter
{
void operator ()(unsigned char* value) const
{
dtFree(value);
}
};
using NavMeshDataValue = std::unique_ptr<unsigned char, NavMeshDataValueDeleter>;
struct NavMeshData
{
NavMeshDataValue mValue;
int mSize;
NavMeshData() = default;
NavMeshData(unsigned char* value, int size)
: mValue(value)
, mSize(size)
{}
};
}
#endif

@ -0,0 +1,230 @@
#include "navmeshmanager.hpp"
#include "debug.hpp"
#include "exceptions.hpp"
#include "gettilespositions.hpp"
#include "makenavmesh.hpp"
#include "navmeshcacheitem.hpp"
#include "settings.hpp"
#include "sharednavmesh.hpp"
#include <DetourNavMesh.h>
#include <BulletCollision/CollisionShapes/btConcaveShape.h>
#include <iostream>
namespace
{
using DetourNavigator::ChangeType;
ChangeType addChangeType(const ChangeType current, const ChangeType add)
{
return current == add ? current : ChangeType::mixed;
}
}
namespace DetourNavigator
{
NavMeshManager::NavMeshManager(const Settings& settings)
: mSettings(settings)
, mRecastMeshManager(settings)
, mOffMeshConnectionsManager(settings)
, mAsyncNavMeshUpdater(settings, mRecastMeshManager, mOffMeshConnectionsManager)
{}
bool NavMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType)
{
if (!mRecastMeshManager.addObject(id, shape, transform, areaType))
return false;
addChangedTiles(shape, transform, ChangeType::add);
return true;
}
bool NavMeshManager::updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType)
{
if (!mRecastMeshManager.updateObject(id, transform, areaType))
return false;
addChangedTiles(shape, transform, ChangeType::mixed);
return true;
}
bool NavMeshManager::removeObject(const ObjectId id)
{
const auto object = mRecastMeshManager.removeObject(id);
if (!object)
return false;
addChangedTiles(object->mShape, object->mTransform, ChangeType::remove);
return true;
}
bool NavMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform)
{
if (!mRecastMeshManager.addWater(cellPosition, cellSize, transform))
return false;
addChangedTiles(cellSize, transform, ChangeType::add);
return true;
}
bool NavMeshManager::removeWater(const osg::Vec2i& cellPosition)
{
const auto water = mRecastMeshManager.removeWater(cellPosition);
if (!water)
return false;
addChangedTiles(water->mCellSize, water->mTransform, ChangeType::remove);
return true;
}
void NavMeshManager::addAgent(const osg::Vec3f& agentHalfExtents)
{
auto cached = mCache.find(agentHalfExtents);
if (cached != mCache.end())
return;
mCache.insert(std::make_pair(agentHalfExtents,
std::make_shared<NavMeshCacheItem>(makeEmptyNavMesh(mSettings), ++mGenerationCounter)));
log("cache add for agent=", agentHalfExtents);
}
void NavMeshManager::reset(const osg::Vec3f& agentHalfExtents)
{
mCache.erase(agentHalfExtents);
}
void NavMeshManager::addOffMeshConnection(const ObjectId id, const osg::Vec3f& start, const osg::Vec3f& end)
{
if (!mOffMeshConnectionsManager.add(id, OffMeshConnection {start, end}))
return;
const auto startTilePosition = getTilePosition(mSettings, start);
const auto endTilePosition = getTilePosition(mSettings, end);
addChangedTile(startTilePosition, ChangeType::add);
if (startTilePosition != endTilePosition)
addChangedTile(endTilePosition, ChangeType::add);
}
void NavMeshManager::removeOffMeshConnection(const ObjectId id)
{
if (const auto connection = mOffMeshConnectionsManager.remove(id))
{
const auto startTilePosition = getTilePosition(mSettings, connection->mStart);
const auto endTilePosition = getTilePosition(mSettings, connection->mEnd);
addChangedTile(startTilePosition, ChangeType::remove);
if (startTilePosition != endTilePosition)
addChangedTile(endTilePosition, ChangeType::remove);
}
}
void NavMeshManager::update(osg::Vec3f playerPosition, const osg::Vec3f& agentHalfExtents)
{
const auto playerTile = getTilePosition(mSettings, toNavMeshCoordinates(mSettings, playerPosition));
auto& lastRevision = mLastRecastMeshManagerRevision[agentHalfExtents];
auto lastPlayerTile = mPlayerTile.find(agentHalfExtents);
if (lastRevision >= mRecastMeshManager.getRevision() && lastPlayerTile != mPlayerTile.end()
&& lastPlayerTile->second == playerTile)
return;
lastRevision = mRecastMeshManager.getRevision();
if (lastPlayerTile == mPlayerTile.end())
lastPlayerTile = mPlayerTile.insert(std::make_pair(agentHalfExtents, playerTile)).first;
else
lastPlayerTile->second = playerTile;
std::map<TilePosition, ChangeType> tilesToPost;
const auto& cached = getCached(agentHalfExtents);
const auto changedTiles = mChangedTiles.find(agentHalfExtents);
{
const auto locked = cached.lock();
const auto& navMesh = locked->getValue();
if (changedTiles != mChangedTiles.end())
{
for (const auto& tile : changedTiles->second)
if (navMesh.getTileAt(tile.first.x(), tile.first.y(), 0))
{
auto tileToPost = tilesToPost.find(tile.first);
if (tileToPost == tilesToPost.end())
tilesToPost.insert(tile);
else
tileToPost->second = addChangeType(tileToPost->second, tile.second);
}
for (const auto& tile : tilesToPost)
changedTiles->second.erase(tile.first);
if (changedTiles->second.empty())
mChangedTiles.erase(changedTiles);
}
const auto maxTiles = navMesh.getParams()->maxTiles;
mRecastMeshManager.forEachTilePosition([&] (const TilePosition& tile)
{
if (tilesToPost.count(tile))
return;
const auto shouldAdd = shouldAddTile(tile, playerTile, maxTiles);
const auto presentInNavMesh = bool(navMesh.getTileAt(tile.x(), tile.y(), 0));
if (shouldAdd && !presentInNavMesh)
tilesToPost.insert(std::make_pair(tile, ChangeType::add));
else if (!shouldAdd && presentInNavMesh)
tilesToPost.insert(std::make_pair(tile, ChangeType::mixed));
});
}
mAsyncNavMeshUpdater.post(agentHalfExtents, cached, playerTile, tilesToPost);
log("cache update posted for agent=", agentHalfExtents,
" playerTile=", lastPlayerTile->second,
" recastMeshManagerRevision=", lastRevision);
}
void NavMeshManager::wait()
{
mAsyncNavMeshUpdater.wait();
}
SharedNavMeshCacheItem NavMeshManager::getNavMesh(const osg::Vec3f& agentHalfExtents) const
{
return getCached(agentHalfExtents);
}
std::map<osg::Vec3f, SharedNavMeshCacheItem> NavMeshManager::getNavMeshes() const
{
return mCache;
}
void NavMeshManager::addChangedTiles(const btCollisionShape& shape, const btTransform& transform,
const ChangeType changeType)
{
getTilesPositions(shape, transform, mSettings,
[&] (const TilePosition& v) { addChangedTile(v, changeType); });
}
void NavMeshManager::addChangedTiles(const int cellSize, const btTransform& transform,
const ChangeType changeType)
{
if (cellSize == std::numeric_limits<int>::max())
return;
getTilesPositions(cellSize, transform, mSettings,
[&] (const TilePosition& v) { addChangedTile(v, changeType); });
}
void NavMeshManager::addChangedTile(const TilePosition& tilePosition, const ChangeType changeType)
{
for (const auto& cached : mCache)
{
auto& tiles = mChangedTiles[cached.first];
auto tile = tiles.find(tilePosition);
if (tile == tiles.end())
tiles.insert(std::make_pair(tilePosition, changeType));
else
tile->second = addChangeType(tile->second, changeType);
}
}
const SharedNavMeshCacheItem& NavMeshManager::getCached(const osg::Vec3f& agentHalfExtents) const
{
const auto cached = mCache.find(agentHalfExtents);
if (cached != mCache.end())
return cached->second;
std::ostringstream stream;
stream << "Agent with half extents is not found: " << agentHalfExtents;
throw InvalidArgument(stream.str());
}
}

@ -0,0 +1,74 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHMANAGER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHMANAGER_H
#include "asyncnavmeshupdater.hpp"
#include "cachedrecastmeshmanager.hpp"
#include "offmeshconnectionsmanager.hpp"
#include "sharednavmesh.hpp"
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <osg/Vec3f>
#include <map>
#include <memory>
class dtNavMesh;
namespace DetourNavigator
{
class NavMeshManager
{
public:
NavMeshManager(const Settings& settings);
bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType);
bool updateObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType);
bool removeObject(const ObjectId id);
void addAgent(const osg::Vec3f& agentHalfExtents);
bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform);
bool removeWater(const osg::Vec2i& cellPosition);
void reset(const osg::Vec3f& agentHalfExtents);
void addOffMeshConnection(const ObjectId id, const osg::Vec3f& start, const osg::Vec3f& end);
void removeOffMeshConnection(const ObjectId id);
void update(osg::Vec3f playerPosition, const osg::Vec3f& agentHalfExtents);
void wait();
SharedNavMeshCacheItem getNavMesh(const osg::Vec3f& agentHalfExtents) const;
std::map<osg::Vec3f, SharedNavMeshCacheItem> getNavMeshes() const;
private:
const Settings& mSettings;
TileCachedRecastMeshManager mRecastMeshManager;
OffMeshConnectionsManager mOffMeshConnectionsManager;
AsyncNavMeshUpdater mAsyncNavMeshUpdater;
std::map<osg::Vec3f, SharedNavMeshCacheItem> mCache;
std::map<osg::Vec3f, std::map<TilePosition, ChangeType>> mChangedTiles;
std::size_t mGenerationCounter = 0;
std::map<osg::Vec3f, TilePosition> mPlayerTile;
std::map<osg::Vec3f, std::size_t> mLastRecastMeshManagerRevision;
void addChangedTiles(const btCollisionShape& shape, const btTransform& transform, const ChangeType changeType);
void addChangedTiles(const int cellSize, const btTransform& transform, const ChangeType changeType);
void addChangedTile(const TilePosition& tilePosition, const ChangeType changeType);
const SharedNavMeshCacheItem& getCached(const osg::Vec3f& agentHalfExtents) const;
};
}
#endif

@ -0,0 +1,158 @@
#include "navmeshtilescache.hpp"
#include "exceptions.hpp"
namespace DetourNavigator
{
namespace
{
inline std::string makeNavMeshKey(const RecastMesh& recastMesh,
const std::vector<OffMeshConnection>& offMeshConnections)
{
std::string result;
result.reserve(
recastMesh.getIndices().size() * sizeof(int)
+ recastMesh.getVertices().size() * sizeof(float)
+ recastMesh.getAreaTypes().size() * sizeof(AreaType)
+ recastMesh.getWater().size() * sizeof(RecastMesh::Water)
+ offMeshConnections.size() * sizeof(OffMeshConnection)
);
std::copy(
reinterpret_cast<const char*>(recastMesh.getIndices().data()),
reinterpret_cast<const char*>(recastMesh.getIndices().data() + recastMesh.getIndices().size()),
std::back_inserter(result)
);
std::copy(
reinterpret_cast<const char*>(recastMesh.getVertices().data()),
reinterpret_cast<const char*>(recastMesh.getVertices().data() + recastMesh.getVertices().size()),
std::back_inserter(result)
);
std::copy(
reinterpret_cast<const char*>(recastMesh.getAreaTypes().data()),
reinterpret_cast<const char*>(recastMesh.getAreaTypes().data() + recastMesh.getAreaTypes().size()),
std::back_inserter(result)
);
std::copy(
reinterpret_cast<const char*>(recastMesh.getWater().data()),
reinterpret_cast<const char*>(recastMesh.getWater().data() + recastMesh.getWater().size()),
std::back_inserter(result)
);
std::copy(
reinterpret_cast<const char*>(offMeshConnections.data()),
reinterpret_cast<const char*>(offMeshConnections.data() + offMeshConnections.size()),
std::back_inserter(result)
);
return result;
}
}
NavMeshTilesCache::NavMeshTilesCache(const std::size_t maxNavMeshDataSize)
: mMaxNavMeshDataSize(maxNavMeshDataSize), mUsedNavMeshDataSize(0), mFreeNavMeshDataSize(0) {}
NavMeshTilesCache::Value NavMeshTilesCache::get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile,
const RecastMesh& recastMesh, const std::vector<OffMeshConnection>& offMeshConnections)
{
const std::lock_guard<std::mutex> lock(mMutex);
const auto agentValues = mValues.find(agentHalfExtents);
if (agentValues == mValues.end())
return Value();
const auto tileValues = agentValues->second.find(changedTile);
if (tileValues == agentValues->second.end())
return Value();
const auto tile = tileValues->second.find(makeNavMeshKey(recastMesh, offMeshConnections));
if (tile == tileValues->second.end())
return Value();
acquireItemUnsafe(tile->second);
return Value(*this, tile->second);
}
NavMeshTilesCache::Value NavMeshTilesCache::set(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile,
const RecastMesh& recastMesh, const std::vector<OffMeshConnection>& offMeshConnections,
NavMeshData&& value)
{
const auto navMeshSize = static_cast<std::size_t>(value.mSize);
const std::lock_guard<std::mutex> lock(mMutex);
if (navMeshSize > mMaxNavMeshDataSize)
return Value();
if (navMeshSize > mFreeNavMeshDataSize + (mMaxNavMeshDataSize - mUsedNavMeshDataSize))
return Value();
while (!mFreeItems.empty() && mUsedNavMeshDataSize + navMeshSize > mMaxNavMeshDataSize)
removeLeastRecentlyUsed();
const auto navMeshKey = makeNavMeshKey(recastMesh, offMeshConnections);
const auto iterator = mFreeItems.emplace(mFreeItems.end(), agentHalfExtents, changedTile, navMeshKey);
const auto emplaced = mValues[agentHalfExtents][changedTile].emplace(navMeshKey, iterator);
if (!emplaced.second)
{
mFreeItems.erase(iterator);
throw InvalidArgument("Set existing cache value");
}
iterator->mNavMeshData = std::move(value);
mUsedNavMeshDataSize += navMeshSize;
mFreeNavMeshDataSize += navMeshSize;
acquireItemUnsafe(iterator);
return Value(*this, iterator);
}
void NavMeshTilesCache::removeLeastRecentlyUsed()
{
const auto& item = mFreeItems.back();
const auto agentValues = mValues.find(item.mAgentHalfExtents);
if (agentValues == mValues.end())
return;
const auto tileValues = agentValues->second.find(item.mChangedTile);
if (tileValues == agentValues->second.end())
return;
const auto value = tileValues->second.find(item.mNavMeshKey);
if (value == tileValues->second.end())
return;
mUsedNavMeshDataSize -= static_cast<std::size_t>(item.mNavMeshData.mSize);
mFreeItems.pop_back();
tileValues->second.erase(value);
if (!tileValues->second.empty())
return;
agentValues->second.erase(tileValues);
if (!agentValues->second.empty())
return;
mValues.erase(agentValues);
}
void NavMeshTilesCache::acquireItemUnsafe(ItemIterator iterator)
{
if (++iterator->mUseCount > 1)
return;
mBusyItems.splice(mBusyItems.end(), mFreeItems, iterator);
mFreeNavMeshDataSize -= static_cast<std::size_t>(iterator->mNavMeshData.mSize);
}
void NavMeshTilesCache::releaseItem(ItemIterator iterator)
{
if (--iterator->mUseCount > 0)
return;
const std::lock_guard<std::mutex> lock(mMutex);
mFreeItems.splice(mFreeItems.begin(), mBusyItems, iterator);
mFreeNavMeshDataSize += static_cast<std::size_t>(iterator->mNavMeshData.mSize);
}
}

@ -0,0 +1,127 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILESCACHE_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_NAVMESHTILESCACHE_H
#include "offmeshconnection.hpp"
#include "navmeshdata.hpp"
#include "recastmesh.hpp"
#include "tileposition.hpp"
#include <atomic>
#include <map>
#include <list>
#include <mutex>
namespace DetourNavigator
{
struct NavMeshDataRef
{
unsigned char* mValue;
int mSize;
};
class NavMeshTilesCache
{
public:
struct Item
{
std::atomic<std::int64_t> mUseCount;
osg::Vec3f mAgentHalfExtents;
TilePosition mChangedTile;
std::string mNavMeshKey;
NavMeshData mNavMeshData;
Item(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile, std::string navMeshKey)
: mUseCount(0)
, mAgentHalfExtents(agentHalfExtents)
, mChangedTile(changedTile)
, mNavMeshKey(std::move(navMeshKey))
{}
};
using ItemIterator = std::list<Item>::iterator;
class Value
{
public:
Value()
: mOwner(nullptr), mIterator() {}
Value(NavMeshTilesCache& owner, ItemIterator iterator)
: mOwner(&owner), mIterator(iterator)
{
}
Value(const Value& other) = delete;
Value(Value&& other)
: mOwner(other.mOwner), mIterator(other.mIterator)
{
other.mIterator = ItemIterator();
}
~Value()
{
if (mIterator != ItemIterator())
mOwner->releaseItem(mIterator);
}
Value& operator =(const Value& other) = delete;
Value& operator =(Value&& other)
{
if (mIterator == other.mIterator)
return *this;
if (mIterator != ItemIterator())
mOwner->releaseItem(mIterator);
mOwner = other.mOwner;
mIterator = other.mIterator;
other.mIterator = ItemIterator();
return *this;
}
NavMeshDataRef get() const
{
return NavMeshDataRef {mIterator->mNavMeshData.mValue.get(), mIterator->mNavMeshData.mSize};
}
operator bool() const
{
return mIterator != ItemIterator();
}
private:
NavMeshTilesCache* mOwner;
ItemIterator mIterator;
};
NavMeshTilesCache(const std::size_t maxNavMeshDataSize);
Value get(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile,
const RecastMesh& recastMesh, const std::vector<OffMeshConnection>& offMeshConnections);
Value set(const osg::Vec3f& agentHalfExtents, const TilePosition& changedTile,
const RecastMesh& recastMesh, const std::vector<OffMeshConnection>& offMeshConnections,
NavMeshData&& value);
private:
std::mutex mMutex;
std::size_t mMaxNavMeshDataSize;
std::size_t mUsedNavMeshDataSize;
std::size_t mFreeNavMeshDataSize;
std::list<Item> mBusyItems;
std::list<Item> mFreeItems;
std::map<osg::Vec3f, std::map<TilePosition, std::map<std::string, ItemIterator>>> mValues;
void removeLeastRecentlyUsed();
void acquireItemUnsafe(ItemIterator iterator);
void releaseItem(ItemIterator iterator);
};
}
#endif

@ -0,0 +1,50 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OBJECTID_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OBJECTID_H
#include <cstddef>
#include <unordered_map>
namespace DetourNavigator
{
class ObjectId
{
public:
template <class T>
explicit ObjectId(const T value) throw()
: mValue(reinterpret_cast<std::size_t>(value))
{
}
std::size_t value() const throw()
{
return mValue;
}
friend bool operator <(const ObjectId lhs, const ObjectId rhs) throw()
{
return lhs.mValue < rhs.mValue;
}
friend bool operator ==(const ObjectId lhs, const ObjectId rhs) throw()
{
return lhs.mValue == rhs.mValue;
}
private:
std::size_t mValue;
};
}
namespace std
{
template <>
struct hash<DetourNavigator::ObjectId>
{
std::size_t operator ()(const DetourNavigator::ObjectId value) const throw()
{
return value.value();
}
};
}
#endif

@ -0,0 +1,15 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OFFMESHCONNECTION_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OFFMESHCONNECTION_H
#include <osg/Vec3f>
namespace DetourNavigator
{
struct OffMeshConnection
{
osg::Vec3f mStart;
osg::Vec3f mEnd;
};
}
#endif

@ -0,0 +1,115 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_OFFMESHCONNECTIONSMANAGER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_OFFMESHCONNECTIONSMANAGER_H
#include "settings.hpp"
#include "settingsutils.hpp"
#include "tileposition.hpp"
#include "objectid.hpp"
#include "offmeshconnection.hpp"
#include <components/misc/guarded.hpp>
#include <osg/Vec3f>
#include <boost/optional.hpp>
#include <map>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace DetourNavigator
{
class OffMeshConnectionsManager
{
public:
OffMeshConnectionsManager(const Settings& settings)
: mSettings(settings)
{}
bool add(const ObjectId id, const OffMeshConnection& value)
{
const auto values = mValues.lock();
if (!values->mById.insert(std::make_pair(id, value)).second)
return false;
const auto startTilePosition = getTilePosition(mSettings, value.mStart);
const auto endTilePosition = getTilePosition(mSettings, value.mEnd);
values->mByTilePosition[startTilePosition].insert(id);
if (startTilePosition != endTilePosition)
values->mByTilePosition[endTilePosition].insert(id);
return true;
}
boost::optional<OffMeshConnection> remove(const ObjectId id)
{
const auto values = mValues.lock();
const auto itById = values->mById.find(id);
if (itById == values->mById.end())
return boost::none;
const auto result = itById->second;
values->mById.erase(itById);
const auto startTilePosition = getTilePosition(mSettings, result.mStart);
const auto endTilePosition = getTilePosition(mSettings, result.mEnd);
removeByTilePosition(values->mByTilePosition, startTilePosition, id);
if (startTilePosition != endTilePosition)
removeByTilePosition(values->mByTilePosition, endTilePosition, id);
return result;
}
std::vector<OffMeshConnection> get(const TilePosition& tilePosition)
{
std::vector<OffMeshConnection> result;
const auto values = mValues.lock();
const auto itByTilePosition = values->mByTilePosition.find(tilePosition);
if (itByTilePosition == values->mByTilePosition.end())
return result;
std::for_each(itByTilePosition->second.begin(), itByTilePosition->second.end(),
[&] (const ObjectId v)
{
const auto itById = values->mById.find(v);
if (itById != values->mById.end())
result.push_back(itById->second);
});
return result;
}
private:
struct Values
{
std::unordered_map<ObjectId, OffMeshConnection> mById;
std::map<TilePosition, std::unordered_set<ObjectId>> mByTilePosition;
};
const Settings& mSettings;
Misc::ScopeGuarded<Values> mValues;
void removeByTilePosition(std::map<TilePosition, std::unordered_set<ObjectId>>& valuesByTilePosition,
const TilePosition& tilePosition, const ObjectId id)
{
const auto it = valuesByTilePosition.find(tilePosition);
if (it != valuesByTilePosition.end())
it->second.erase(id);
}
};
}
#endif

@ -0,0 +1,22 @@
#include "recastmesh.hpp"
#include "exceptions.hpp"
#include <Recast.h>
namespace DetourNavigator
{
RecastMesh::RecastMesh(std::vector<int> indices, std::vector<float> vertices, std::vector<AreaType> areaTypes,
std::vector<Water> water, const std::size_t trianglesPerChunk)
: mIndices(std::move(indices))
, mVertices(std::move(vertices))
, mAreaTypes(std::move(areaTypes))
, mWater(std::move(water))
, mChunkyTriMesh(mVertices, mIndices, mAreaTypes, trianglesPerChunk)
{
if (getTrianglesCount() != mAreaTypes.size())
throw InvalidArgument("Number of flags doesn't match number of triangles: triangles="
+ std::to_string(getTrianglesCount()) + ", areaTypes=" + std::to_string(mAreaTypes.size()));
if (getVerticesCount())
rcCalcBounds(mVertices.data(), static_cast<int>(getVerticesCount()), mBounds.mMin.ptr(), mBounds.mMax.ptr());
}
}

@ -0,0 +1,80 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESH_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESH_H
#include "areatype.hpp"
#include "chunkytrimesh.hpp"
#include "bounds.hpp"
#include <memory>
#include <string>
#include <vector>
#include <osg/Vec3f>
#include <LinearMath/btTransform.h>
namespace DetourNavigator
{
class RecastMesh
{
public:
struct Water
{
int mCellSize;
btTransform mTransform;
};
RecastMesh(std::vector<int> indices, std::vector<float> vertices, std::vector<AreaType> areaTypes,
std::vector<Water> water, const std::size_t trianglesPerChunk);
const std::vector<int>& getIndices() const
{
return mIndices;
}
const std::vector<float>& getVertices() const
{
return mVertices;
}
const std::vector<AreaType>& getAreaTypes() const
{
return mAreaTypes;
}
const std::vector<Water>& getWater() const
{
return mWater;
}
std::size_t getVerticesCount() const
{
return mVertices.size() / 3;
}
std::size_t getTrianglesCount() const
{
return mIndices.size() / 3;
}
const ChunkyTriMesh& getChunkyTriMesh() const
{
return mChunkyTriMesh;
}
const Bounds& getBounds() const
{
return mBounds;
}
private:
std::vector<int> mIndices;
std::vector<float> mVertices;
std::vector<AreaType> mAreaTypes;
std::vector<Water> mWater;
ChunkyTriMesh mChunkyTriMesh;
Bounds mBounds;
};
}
#endif

@ -0,0 +1,183 @@
#include "recastmeshbuilder.hpp"
#include "chunkytrimesh.hpp"
#include "debug.hpp"
#include "settings.hpp"
#include "settingsutils.hpp"
#include "exceptions.hpp"
#include <components/bullethelpers/processtrianglecallback.hpp>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionShapes/btConcaveShape.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <LinearMath/btTransform.h>
#include <algorithm>
namespace
{
osg::Vec3f makeOsgVec3f(const btVector3& value)
{
return osg::Vec3f(value.x(), value.y(), value.z());
}
}
namespace DetourNavigator
{
using BulletHelpers::makeProcessTriangleCallback;
RecastMeshBuilder::RecastMeshBuilder(const Settings& settings, const TileBounds& bounds)
: mSettings(settings)
, mBounds(bounds)
{
mBounds.mMin /= mSettings.get().mRecastScaleFactor;
mBounds.mMax /= mSettings.get().mRecastScaleFactor;
}
void RecastMeshBuilder::addObject(const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType)
{
if (shape.isCompound())
return addObject(static_cast<const btCompoundShape&>(shape), transform, areaType);
else if (shape.getShapeType() == TERRAIN_SHAPE_PROXYTYPE)
return addObject(static_cast<const btHeightfieldTerrainShape&>(shape), transform, areaType);
else if (shape.isConcave())
return addObject(static_cast<const btConcaveShape&>(shape), transform, areaType);
else if (shape.getShapeType() == BOX_SHAPE_PROXYTYPE)
return addObject(static_cast<const btBoxShape&>(shape), transform, areaType);
std::ostringstream message;
message << "Unsupported shape type: " << BroadphaseNativeTypes(shape.getShapeType());
throw InvalidArgument(message.str());
}
void RecastMeshBuilder::addObject(const btCompoundShape& shape, const btTransform& transform,
const AreaType areaType)
{
for (int i = 0, num = shape.getNumChildShapes(); i < num; ++i)
addObject(*shape.getChildShape(i), transform * shape.getChildTransform(i), areaType);
}
void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform,
const AreaType areaType)
{
return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* triangle, int, int)
{
for (std::size_t i = 3; i > 0; --i)
addTriangleVertex(transform(triangle[i - 1]));
mAreaTypes.push_back(areaType);
}));
}
void RecastMeshBuilder::addObject(const btHeightfieldTerrainShape& shape, const btTransform& transform,
const AreaType areaType)
{
return addObject(shape, transform, makeProcessTriangleCallback([&] (btVector3* triangle, int, int)
{
for (std::size_t i = 0; i < 3; ++i)
addTriangleVertex(transform(triangle[i]));
mAreaTypes.push_back(areaType);
}));
}
void RecastMeshBuilder::addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType)
{
const auto indexOffset = static_cast<int>(mVertices.size() / 3);
for (int vertex = 0, count = shape.getNumVertices(); vertex < count; ++vertex)
{
btVector3 position;
shape.getVertex(vertex, position);
addVertex(transform(position));
}
const std::array<int, 36> indices {{
0, 2, 3,
3, 1, 0,
0, 4, 6,
6, 2, 0,
0, 1, 5,
5, 4, 0,
7, 5, 1,
1, 3, 7,
7, 3, 2,
2, 6, 7,
7, 6, 4,
4, 5, 7,
}};
std::transform(indices.begin(), indices.end(), std::back_inserter(mIndices),
[&] (int index) { return index + indexOffset; });
std::generate_n(std::back_inserter(mAreaTypes), 12, [=] { return areaType; });
}
void RecastMeshBuilder::addWater(const int cellSize, const btTransform& transform)
{
mWater.push_back(RecastMesh::Water {cellSize, transform});
}
std::shared_ptr<RecastMesh> RecastMeshBuilder::create() const
{
return std::make_shared<RecastMesh>(mIndices, mVertices, mAreaTypes, mWater, mSettings.get().mTrianglesPerChunk);
}
void RecastMeshBuilder::reset()
{
mIndices.clear();
mVertices.clear();
mAreaTypes.clear();
mWater.clear();
}
void RecastMeshBuilder::addObject(const btConcaveShape& shape, const btTransform& transform,
btTriangleCallback&& callback)
{
btVector3 aabbMin;
btVector3 aabbMax;
shape.getAabb(btTransform::getIdentity(), aabbMin, aabbMax);
aabbMin = transform(aabbMin);
aabbMax = transform(aabbMax);
aabbMin.setX(std::max(mBounds.mMin.x(), aabbMin.x()));
aabbMin.setX(std::min(mBounds.mMax.x(), aabbMin.x()));
aabbMin.setY(std::max(mBounds.mMin.y(), aabbMin.y()));
aabbMin.setY(std::min(mBounds.mMax.y(), aabbMin.y()));
aabbMax.setX(std::max(mBounds.mMin.x(), aabbMax.x()));
aabbMax.setX(std::min(mBounds.mMax.x(), aabbMax.x()));
aabbMax.setY(std::max(mBounds.mMin.y(), aabbMax.y()));
aabbMax.setY(std::min(mBounds.mMax.y(), aabbMax.y()));
const auto inversedTransform = transform.inverse();
aabbMin = inversedTransform(aabbMin);
aabbMax = inversedTransform(aabbMax);
aabbMin.setX(std::min(aabbMin.x(), aabbMax.x()));
aabbMin.setY(std::min(aabbMin.y(), aabbMax.y()));
aabbMin.setZ(std::min(aabbMin.z(), aabbMax.z()));
aabbMax.setX(std::max(aabbMin.x(), aabbMax.x()));
aabbMax.setY(std::max(aabbMin.y(), aabbMax.y()));
aabbMax.setZ(std::max(aabbMin.z(), aabbMax.z()));
shape.processAllTriangles(&callback, aabbMin, aabbMax);
}
void RecastMeshBuilder::addTriangleVertex(const btVector3& worldPosition)
{
mIndices.push_back(static_cast<int>(mVertices.size() / 3));
addVertex(worldPosition);
}
void RecastMeshBuilder::addVertex(const btVector3& worldPosition)
{
const auto navMeshPosition = toNavMeshCoordinates(mSettings, makeOsgVec3f(worldPosition));
mVertices.push_back(navMeshPosition.x());
mVertices.push_back(navMeshPosition.y());
mVertices.push_back(navMeshPosition.z());
}
}

@ -0,0 +1,57 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHBUILDER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHBUILDER_H
#include "recastmesh.hpp"
#include "tilebounds.hpp"
#include <LinearMath/btTransform.h>
class btBoxShape;
class btCollisionShape;
class btCompoundShape;
class btConcaveShape;
class btHeightfieldTerrainShape;
class btTriangleCallback;
namespace DetourNavigator
{
struct Settings;
class RecastMeshBuilder
{
public:
RecastMeshBuilder(const Settings& settings, const TileBounds& bounds);
void addObject(const btCollisionShape& shape, const btTransform& transform, const AreaType areaType);
void addObject(const btCompoundShape& shape, const btTransform& transform, const AreaType areaType);
void addObject(const btConcaveShape& shape, const btTransform& transform, const AreaType areaType);
void addObject(const btHeightfieldTerrainShape& shape, const btTransform& transform, const AreaType areaType);
void addObject(const btBoxShape& shape, const btTransform& transform, const AreaType areaType);
void addWater(const int mCellSize, const btTransform& transform);
std::shared_ptr<RecastMesh> create() const;
void reset();
private:
std::reference_wrapper<const Settings> mSettings;
TileBounds mBounds;
std::vector<int> mIndices;
std::vector<float> mVertices;
std::vector<AreaType> mAreaTypes;
std::vector<RecastMesh::Water> mWater;
void addObject(const btConcaveShape& shape, const btTransform& transform, btTriangleCallback&& callback);
void addTriangleVertex(const btVector3& worldPosition);
void addVertex(const btVector3& worldPosition);
};
}
#endif

@ -0,0 +1,97 @@
#include "recastmeshmanager.hpp"
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
namespace DetourNavigator
{
RecastMeshManager::RecastMeshManager(const Settings& settings, const TileBounds& bounds)
: mShouldRebuild(false)
, mMeshBuilder(settings, bounds)
{
}
bool RecastMeshManager::addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType)
{
const auto iterator = mObjectsOrder.emplace(mObjectsOrder.end(), RecastMeshObject(shape, transform, areaType));
if (!mObjects.emplace(id, iterator).second)
{
mObjectsOrder.erase(iterator);
return false;
}
mShouldRebuild = true;
return mShouldRebuild;
}
bool RecastMeshManager::updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType)
{
const auto object = mObjects.find(id);
if (object == mObjects.end())
return false;
if (!object->second->update(transform, areaType))
return false;
mShouldRebuild = true;
return mShouldRebuild;
}
boost::optional<RemovedRecastMeshObject> RecastMeshManager::removeObject(const ObjectId id)
{
const auto object = mObjects.find(id);
if (object == mObjects.end())
return boost::none;
const RemovedRecastMeshObject result {object->second->getShape(), object->second->getTransform()};
mObjectsOrder.erase(object->second);
mObjects.erase(object);
mShouldRebuild = true;
return result;
}
bool RecastMeshManager::addWater(const osg::Vec2i& cellPosition, const int cellSize,
const btTransform& transform)
{
const auto iterator = mWaterOrder.emplace(mWaterOrder.end(), Water {cellSize, transform});
if (!mWater.emplace(cellPosition, iterator).second)
{
mWaterOrder.erase(iterator);
return false;
}
mShouldRebuild = true;
return true;
}
boost::optional<RecastMeshManager::Water> RecastMeshManager::removeWater(const osg::Vec2i& cellPosition)
{
const auto water = mWater.find(cellPosition);
if (water == mWater.end())
return boost::none;
mShouldRebuild = true;
const auto result = *water->second;
mWaterOrder.erase(water->second);
mWater.erase(water);
return result;
}
std::shared_ptr<RecastMesh> RecastMeshManager::getMesh()
{
rebuild();
return mMeshBuilder.create();
}
bool RecastMeshManager::isEmpty() const
{
return mObjects.empty();
}
void RecastMeshManager::rebuild()
{
if (!mShouldRebuild)
return;
mMeshBuilder.reset();
for (const auto& v : mWaterOrder)
mMeshBuilder.addWater(v.mCellSize, v.mTransform);
for (const auto& v : mObjectsOrder)
mMeshBuilder.addObject(v.getShape(), v.getTransform(), v.getAreaType());
mShouldRebuild = false;
}
}

@ -0,0 +1,66 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHMANAGER_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_RECASTMESHMANAGER_H
#include "recastmeshbuilder.hpp"
#include "recastmeshobject.hpp"
#include "objectid.hpp"
#include <LinearMath/btTransform.h>
#include <osg/Vec2i>
#include <boost/optional.hpp>
#include <map>
#include <unordered_map>
#include <list>
class btCollisionShape;
namespace DetourNavigator
{
struct RemovedRecastMeshObject
{
std::reference_wrapper<const btCollisionShape> mShape;
btTransform mTransform;
};
class RecastMeshManager
{
public:
struct Water
{
int mCellSize;
btTransform mTransform;
};
RecastMeshManager(const Settings& settings, const TileBounds& bounds);
bool addObject(const ObjectId id, const btCollisionShape& shape, const btTransform& transform,
const AreaType areaType);
bool updateObject(const ObjectId id, const btTransform& transform, const AreaType areaType);
bool addWater(const osg::Vec2i& cellPosition, const int cellSize, const btTransform& transform);
boost::optional<Water> removeWater(const osg::Vec2i& cellPosition);
boost::optional<RemovedRecastMeshObject> removeObject(const ObjectId id);
std::shared_ptr<RecastMesh> getMesh();
bool isEmpty() const;
private:
bool mShouldRebuild;
RecastMeshBuilder mMeshBuilder;
std::list<RecastMeshObject> mObjectsOrder;
std::unordered_map<ObjectId, std::list<RecastMeshObject>::iterator> mObjects;
std::list<Water> mWaterOrder;
std::map<osg::Vec2i, std::list<Water>::iterator> mWater;
void rebuild();
};
}
#endif

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save