Browse Source

Merge pull request #2061 from eugeneko/navmesh-streaming

Navmesh streaming
Eugene Kozlov 8 years ago
parent
commit
d537c2027a

+ 80 - 2
Source/Samples/15_Navigation/Navigation.cpp

@@ -48,7 +48,9 @@ URHO3D_DEFINE_APPLICATION_MAIN(Navigation)
 
 Navigation::Navigation(Context* context) :
     Sample(context),
-    drawDebug_(false)
+    drawDebug_(false),
+    useStreaming_(false),
+    streamingDistance_(2)
 {
 }
 
@@ -141,6 +143,8 @@ void Navigation::CreateScene()
 
     // Create a NavigationMesh component to the scene root
     NavigationMesh* navMesh = scene_->CreateComponent<NavigationMesh>();
+    // Set small tiles to show navigation mesh streaming
+    navMesh->SetTileSize(32);
     // Create a Navigable component to the scene root. This tags all of the geometry in the scene as being part of the
     // navigation mesh. By default this is recursive, but the recursion could be turned off from Navigable
     scene_->CreateComponent<Navigable>();
@@ -185,6 +189,7 @@ void Navigation::CreateUI()
         "Use WASD keys to move, RMB to rotate view\n"
         "LMB to set destination, SHIFT+LMB to teleport\n"
         "MMB or O key to add or remove obstacles\n"
+        "Tab to toggle navigation mesh streaming\n"
         "Space to toggle debug geometry"
     );
     instructionText->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 15);
@@ -299,7 +304,7 @@ void Navigation::AddOrRemoveObject()
     Vector3 hitPos;
     Drawable* hitDrawable;
 
-    if (Raycast(250.0f, hitPos, hitDrawable))
+    if (!useStreaming_ && Raycast(250.0f, hitPos, hitDrawable))
     {
         // The part of the navigation mesh we must update, which is the world bounding box of the associated
         // drawable component
@@ -390,6 +395,69 @@ void Navigation::FollowPath(float timeStep)
     }
 }
 
+void Navigation::ToggleStreaming(bool enabled)
+{
+    NavigationMesh* navMesh = scene_->GetComponent<NavigationMesh>();
+    if (enabled)
+    {
+        int maxTiles = (2 * streamingDistance_ + 1) * (2 * streamingDistance_ + 1);
+        BoundingBox boundingBox = navMesh->GetBoundingBox();
+        SaveNavigationData();
+        navMesh->Allocate(boundingBox, maxTiles);
+    }
+    else
+        navMesh->Build();
+}
+
+void Navigation::UpdateStreaming()
+{
+    // Center the navigation mesh at the jack
+    NavigationMesh* navMesh = scene_->GetComponent<NavigationMesh>();
+    const IntVector2 jackTile = navMesh->GetTileIndex(jackNode_->GetWorldPosition());
+    const IntVector2 numTiles = navMesh->GetNumTiles();
+    const IntVector2 beginTile = VectorMax(IntVector2::ZERO, jackTile - IntVector2::ONE * streamingDistance_);
+    const IntVector2 endTile = VectorMin(jackTile + IntVector2::ONE * streamingDistance_, numTiles - IntVector2::ONE);
+
+    // Remove tiles
+    for (HashSet<IntVector2>::Iterator i = addedTiles_.Begin(); i != addedTiles_.End();)
+    {
+        const IntVector2 tileIdx = *i;
+        if (beginTile.x_ <= tileIdx.x_ && tileIdx.x_ <= endTile.x_ && beginTile.y_ <= tileIdx.y_ && tileIdx.y_ <= endTile.y_)
+            ++i;
+        else
+        {
+            navMesh->RemoveTile(tileIdx);
+            i = addedTiles_.Erase(i);
+        }
+    }
+
+    // Add tiles
+    for (int z = beginTile.y_; z <= endTile.y_; ++z)
+        for (int x = beginTile.x_; x <= endTile.x_; ++x)
+        {
+            const IntVector2 tileIdx(x, z);
+            if (!navMesh->HasTile(tileIdx) && tileData_.Contains(tileIdx))
+            {
+                addedTiles_.Insert(tileIdx);
+                navMesh->AddTile(tileData_[tileIdx]);
+            }
+        }
+}
+
+void Navigation::SaveNavigationData()
+{
+    NavigationMesh* navMesh = scene_->GetComponent<NavigationMesh>();
+    tileData_.Clear();
+    addedTiles_.Clear();
+    const IntVector2 numTiles = navMesh->GetNumTiles();
+    for (int z = 0; z < numTiles.y_; ++z)
+        for (int x = 0; x <= numTiles.x_; ++x)
+        {
+            const IntVector2 tileIdx = IntVector2(x, z);
+            tileData_[tileIdx] = navMesh->GetTileData(tileIdx);
+        }
+}
+
 void Navigation::HandleUpdate(StringHash eventType, VariantMap& eventData)
 {
     using namespace Update;
@@ -402,6 +470,16 @@ void Navigation::HandleUpdate(StringHash eventType, VariantMap& eventData)
 
     // Make Jack follow the Detour path
     FollowPath(timeStep);
+
+    // Update streaming
+    Input* input = GetSubsystem<Input>();
+    if (input->GetKeyPress(KEY_TAB))
+    {
+        useStreaming_ = !useStreaming_;
+        ToggleStreaming(useStreaming_);
+    }
+    if (useStreaming_)
+        UpdateStreaming();
 }
 
 void Navigation::HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)

+ 14 - 0
Source/Samples/15_Navigation/Navigation.h

@@ -146,6 +146,12 @@ private:
     bool Raycast(float maxDistance, Vector3& hitPos, Drawable*& hitDrawable);
     /// Make Jack follow the Detour path.
     void FollowPath(float timeStep);
+    /// Toggle navigation mesh streaming.
+    void ToggleStreaming(bool enabled);
+    /// Update navigation mesh streaming.
+    void UpdateStreaming();
+    /// Save navigation data for streaming.
+    void SaveNavigationData();
     /// Handle the logic update event.
     void HandleUpdate(StringHash eventType, VariantMap& eventData);
     /// Handle the post-render update event.
@@ -159,4 +165,12 @@ private:
     SharedPtr<Node> jackNode_;
     /// Flag for drawing debug geometry.
     bool drawDebug_;
+    /// Flag for using navigation mesh streaming.
+    bool useStreaming_;
+    /// Streaming distance.
+    int streamingDistance_;
+    /// Tile data.
+    HashMap<IntVector2, PODVector<unsigned char> > tileData_;
+    /// Added tiles.
+    HashSet<IntVector2> addedTiles_;
 };

+ 88 - 0
Source/Samples/39_CrowdNavigation/CrowdNavigation.cpp

@@ -55,6 +55,7 @@ URHO3D_DEFINE_APPLICATION_MAIN(CrowdNavigation)
 
 CrowdNavigation::CrowdNavigation(Context* context) :
     Sample(context),
+    streamingDistance_(2),
     drawDebug_(false)
 {
 }
@@ -135,6 +136,8 @@ void CrowdNavigation::CreateScene()
 
     // Create a DynamicNavigationMesh component to the scene root
     DynamicNavigationMesh* navMesh = scene_->CreateComponent<DynamicNavigationMesh>();
+    // Set small tiles to show navigation mesh streaming
+    navMesh->SetTileSize(32);
     // Enable drawing debug geometry for obstacles and off-mesh connections
     navMesh->SetDrawObstacles(true);
     navMesh->SetDrawOffMeshConnections(true);
@@ -213,6 +216,7 @@ void CrowdNavigation::CreateUI()
         "LMB to set destination, SHIFT+LMB to spawn a Jack\n"
         "MMB or O key to add obstacles or remove obstacles/agents\n"
         "F5 to save scene, F7 to load\n"
+        "Tab to toggle navigation mesh streaming\n"
         "Space to toggle debug geometry\n"
         "F12 to toggle this instruction text"
     );
@@ -473,6 +477,79 @@ void CrowdNavigation::MoveCamera(float timeStep)
     }
 }
 
+void CrowdNavigation::ToggleStreaming(bool enabled)
+{
+    DynamicNavigationMesh* navMesh = scene_->GetComponent<DynamicNavigationMesh>();
+    if (enabled)
+    {
+        int maxTiles = (2 * streamingDistance_ + 1) * (2 * streamingDistance_ + 1);
+        BoundingBox boundingBox = navMesh->GetBoundingBox();
+        SaveNavigationData();
+        navMesh->Allocate(boundingBox, maxTiles);
+    }
+    else
+        navMesh->Build();
+}
+
+void CrowdNavigation::UpdateStreaming()
+{
+    // Center the navigation mesh at the crowd of jacks
+    Vector3 averageJackPosition;
+    if (Node* jackGroup = scene_->GetChild("Jacks"))
+    {
+        const unsigned numJacks = jackGroup->GetNumChildren();
+        for (unsigned i = 0; i < numJacks; ++i)
+            averageJackPosition += jackGroup->GetChild(i)->GetWorldPosition();
+        averageJackPosition /= (float)numJacks;
+    }
+
+    // Compute currently loaded area
+    DynamicNavigationMesh* navMesh = scene_->GetComponent<DynamicNavigationMesh>();
+    const IntVector2 jackTile = navMesh->GetTileIndex(averageJackPosition);
+    const IntVector2 numTiles = navMesh->GetNumTiles();
+    const IntVector2 beginTile = VectorMax(IntVector2::ZERO, jackTile - IntVector2::ONE * streamingDistance_);
+    const IntVector2 endTile = VectorMin(jackTile + IntVector2::ONE * streamingDistance_, numTiles - IntVector2::ONE);
+
+    // Remove tiles
+    for (HashSet<IntVector2>::Iterator i = addedTiles_.Begin(); i != addedTiles_.End();)
+    {
+        const IntVector2 tileIdx = *i;
+        if (beginTile.x_ <= tileIdx.x_ && tileIdx.x_ <= endTile.x_ && beginTile.y_ <= tileIdx.y_ && tileIdx.y_ <= endTile.y_)
+            ++i;
+        else
+        {
+            navMesh->RemoveTile(tileIdx);
+            i = addedTiles_.Erase(i);
+        }
+    }
+
+    // Add tiles
+    for (int z = beginTile.y_; z <= endTile.y_; ++z)
+        for (int x = beginTile.x_; x <= endTile.x_; ++x)
+        {
+            const IntVector2 tileIdx(x, z);
+            if (!navMesh->HasTile(tileIdx) && tileData_.Contains(tileIdx))
+            {
+                addedTiles_.Insert(tileIdx);
+                navMesh->AddTile(tileData_[tileIdx]);
+            }
+        }
+}
+
+void CrowdNavigation::SaveNavigationData()
+{
+    DynamicNavigationMesh* navMesh = scene_->GetComponent<DynamicNavigationMesh>();
+    tileData_.Clear();
+    addedTiles_.Clear();
+    const IntVector2 numTiles = navMesh->GetNumTiles();
+    for (int z = 0; z < numTiles.y_; ++z)
+        for (int x = 0; x <= numTiles.x_; ++x)
+        {
+            const IntVector2 tileIdx = IntVector2(x, z);
+            tileData_[tileIdx] = navMesh->GetTileData(tileIdx);
+        }
+}
+
 void CrowdNavigation::HandleUpdate(StringHash eventType, VariantMap& eventData)
 {
     using namespace Update;
@@ -482,6 +559,17 @@ void CrowdNavigation::HandleUpdate(StringHash eventType, VariantMap& eventData)
 
     // Move the camera, scale movement with time step
     MoveCamera(timeStep);
+
+    // Update streaming
+    Input* input = GetSubsystem<Input>();
+    if (input->GetKeyPress(KEY_TAB))
+    {
+        useStreaming_ = !useStreaming_;
+        ToggleStreaming(useStreaming_);
+    }
+    if (useStreaming_)
+        UpdateStreaming();
+
 }
 
 void CrowdNavigation::HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)

+ 14 - 0
Source/Samples/39_CrowdNavigation/CrowdNavigation.h

@@ -152,6 +152,12 @@ private:
     void CreateMovingBarrels(DynamicNavigationMesh* navMesh);
     /// Utility function to raycast to the cursor position. Return true if hit.
     bool Raycast(float maxDistance, Vector3& hitPos, Drawable*& hitDrawable);
+    /// Toggle navigation mesh streaming.
+    void ToggleStreaming(bool enabled);
+    /// Update navigation mesh streaming.
+    void UpdateStreaming();
+    /// Save navigation data for streaming.
+    void SaveNavigationData();
     /// Handle the logic update event.
     void HandleUpdate(StringHash eventType, VariantMap& eventData);
     /// Handle the post-render update event.
@@ -163,6 +169,14 @@ private:
     /// Handle crowd agent formation.
     void HandleCrowdAgentFormation(StringHash eventType, VariantMap& eventData);
 
+    /// Flag for using navigation mesh streaming.
+    bool useStreaming_;
+    /// Streaming distance.
+    int streamingDistance_;
+    /// Tile data.
+    HashMap<IntVector2, PODVector<unsigned char> > tileData_;
+    /// Added tiles.
+    HashSet<IntVector2> addedTiles_;
     /// Flag for drawing debug geometry.
     bool drawDebug_;
 };

+ 2 - 0
Source/Urho3D/AngelScript/MathAPI.cpp

@@ -86,6 +86,7 @@ static void RegisterMathFunctions(asIScriptEngine* engine)
     engine->RegisterGlobalFunction("bool IsPowerOfTwo(uint)", asFUNCTION(IsPowerOfTwo), asCALL_CDECL);
     engine->RegisterGlobalFunction("uint NextPowerOfTwo(uint)", asFUNCTION(NextPowerOfTwo), asCALL_CDECL);
     engine->RegisterGlobalFunction("uint CountSetBits(uint)", asFUNCTION(CountSetBits), asCALL_CDECL);
+    engine->RegisterGlobalFunction("uint IntegerLog2(uint)", asFUNCTION(IntegerLog2), asCALL_CDECL);
     engine->RegisterGlobalFunction("uint SDBMHash(uint, uint8)", asFUNCTION(SDBMHash), asCALL_CDECL);
     engine->RegisterGlobalFunction("float Random()", asFUNCTIONPR(Random, (), float), asCALL_CDECL);
     engine->RegisterGlobalFunction("float Random(float)", asFUNCTIONPR(Random, (float), float), asCALL_CDECL);
@@ -1226,6 +1227,7 @@ static void RegisterVolumes(asIScriptEngine* engine)
     engine->RegisterObjectMethod("BoundingBox", "BoundingBox Transformed(const Matrix3&in) const", asMETHODPR(BoundingBox, Transformed, (const Matrix3&) const, BoundingBox), asCALL_THISCALL);
     engine->RegisterObjectMethod("BoundingBox", "BoundingBox Transformed(const Matrix3x4&in) const", asMETHODPR(BoundingBox, Transformed, (const Matrix3x4&) const, BoundingBox), asCALL_THISCALL);
     engine->RegisterObjectMethod("BoundingBox", "Rect Projected(const Matrix4&in) const", asMETHODPR(BoundingBox, Projected, (const Matrix4&) const, Rect), asCALL_THISCALL);
+    engine->RegisterObjectMethod("BoundingBox", "float DistanceToPoint(const Vector3&in) const", asMETHOD(BoundingBox, DistanceToPoint), asCALL_THISCALL);
     engine->RegisterObjectMethod("BoundingBox", "String ToString() const", asMETHOD(BoundingBox, ToString), asCALL_THISCALL);
     engine->RegisterObjectMethod("BoundingBox", "bool Defined() const", asMETHOD(BoundingBox, Defined), asCALL_THISCALL);
     engine->RegisterObjectMethod("BoundingBox", "Vector3 get_center() const", asMETHOD(BoundingBox, Center), asCALL_THISCALL);

+ 21 - 0
Source/Urho3D/AngelScript/NavigationAPI.cpp

@@ -72,6 +72,18 @@ static Vector3 NavigationMeshMoveAlongSurface(const Vector3& start, const Vector
     return ptr->MoveAlongSurface(start, end, extents, maxVisited);
 }
 
+static VectorBuffer NavigationMeshGetTileData(const IntVector2& tile, const NavigationMesh* ptr)
+{
+    VectorBuffer buffer;
+    buffer.SetData(ptr->GetTileData(tile));
+    return buffer;
+}
+
+static bool NavigationMeshAddTile(const VectorBuffer& tileData, NavigationMesh* ptr)
+{
+    return ptr->AddTile(tileData.GetBuffer());
+}
+
 static Vector3 NavigationMeshGetRandomPoint(NavigationMesh* ptr)
 {
     return ptr->GetRandomPoint();
@@ -104,8 +116,17 @@ static Vector3 CrowdManagerRandomPointInCircle(const Vector3& center, float radi
 
 template<class T> static void RegisterNavMeshBase(asIScriptEngine* engine, const char* name)
 {
+    engine->RegisterObjectMethod(name, "bool Allocate(const BoundingBox&in, uint)", asMETHOD(T, Allocate), asCALL_THISCALL);
     engine->RegisterObjectMethod(name, "bool Build()", asMETHODPR(T, Build, (), bool), asCALL_THISCALL);
     engine->RegisterObjectMethod(name, "bool Build(const BoundingBox&in)", asMETHODPR(T, Build, (const BoundingBox&), bool), asCALL_THISCALL);
+    engine->RegisterObjectMethod(name, "bool Build(const IntVector2&, const IntVector2&)", asMETHODPR(T, Build, (const IntVector2&, const IntVector2&), bool), asCALL_THISCALL);
+    engine->RegisterObjectMethod(name, "VectorBuffer GetTileData(const IntVector2&) const", asFUNCTION(NavigationMeshGetTileData), asCALL_CDECL_OBJLAST);
+    engine->RegisterObjectMethod(name, "bool AddTile(const VectorBuffer&in) const", asFUNCTION(NavigationMeshAddTile), asCALL_CDECL_OBJLAST);
+    engine->RegisterObjectMethod(name, "void RemoveTile(const IntVector2&)", asMETHOD(T, RemoveTile), asCALL_THISCALL);
+    engine->RegisterObjectMethod(name, "void RemoveAllTiles()", asMETHOD(T, RemoveAllTiles), asCALL_THISCALL);
+    engine->RegisterObjectMethod(name, "bool HasTile(const IntVector2&) const", asMETHOD(T, HasTile), asCALL_THISCALL);
+    engine->RegisterObjectMethod(name, "BoundingBox GetTileBoudningBox(const IntVector2&) const", asMETHOD(T, GetTileBoudningBox), asCALL_THISCALL);
+    engine->RegisterObjectMethod(name, "IntVector2 GetTileIndex(const Vector3&) const", asMETHOD(T, GetTileIndex), asCALL_THISCALL);
     engine->RegisterObjectMethod(name, "void SetAreaCost(uint, float)", asMETHOD(T, SetAreaCost), asCALL_THISCALL);
     engine->RegisterObjectMethod(name, "float GetAreaCost(uint) const", asMETHOD(T, GetAreaCost), asCALL_THISCALL);
     engine->RegisterObjectMethod(name, "Vector3 FindNearestPoint(const Vector3&in, const Vector3&in extents = Vector3(1.0, 1.0, 1.0))", asFUNCTION(NavigationMeshFindNearestPoint), asCALL_CDECL_OBJLAST);

+ 1 - 0
Source/Urho3D/LuaScript/pkgs/Math/BoundingBox.pkg

@@ -42,6 +42,7 @@ class BoundingBox
     BoundingBox Transformed(const Matrix3& transform) const;
     BoundingBox Transformed(const Matrix3x4& transform) const;
     Rect Projected(const Matrix4& projection) const;
+    float DistanceToPoint(const Vector3& point) const;
 
     Intersection IsInside(const Vector3& point) const;
     Intersection IsInside(const BoundingBox& box) const;

+ 1 - 0
Source/Urho3D/LuaScript/pkgs/Math/MathDefs.pkg

@@ -65,6 +65,7 @@ bool IsPowerOfTwo(unsigned value);
 unsigned NextPowerOfTwo(unsigned value);
 
 unsigned CountSetBits(unsigned value);
+unsigned IntegerLog2(unsigned value);
 
 unsigned SDBMHash(unsigned hash, unsigned char c);
 float Random();

+ 22 - 0
Source/Urho3D/LuaScript/pkgs/Navigation/NavigationMesh.pkg

@@ -1,4 +1,5 @@
 $#include "Navigation/NavigationMesh.h"
+$#include "IO/VectorBuffer.h"
 
 enum NavmeshPartitionType
 {
@@ -31,8 +32,17 @@ class NavigationMesh : public Component
     void SetDetailSampleMaxError(float error);
     void SetPadding(const Vector3& padding);
     void SetAreaCost(unsigned areaID, float cost);
+    bool Allocate(const BoundingBox& boundingBox, unsigned maxTiles);
     bool Build();
     bool Build(const BoundingBox& boundingBox);
+    bool Build(const IntVector2& from, const IntVector2& to);
+    tolua_outside VectorBuffer NavigationMeshGetTileData @ GetTileData(const IntVector2& tile) const;
+    tolua_outside bool NavigationMeshAddTile @ AddTile(const VectorBuffer& tileData);
+    void RemoveTile(const IntVector2& tile);
+    void RemoveAllTiles();
+    bool HasTile(const IntVector2& tile) const;
+    BoundingBox GetTileBoudningBox(const IntVector2& tile) const;
+    IntVector2 GetTileIndex(const Vector3& position) const;
     void SetPartitionType(NavmeshPartitionType aType);
     void SetDrawOffMeshConnections(bool enable);
     void SetDrawNavAreas(bool enable);
@@ -93,6 +103,18 @@ class NavigationMesh : public Component
 };
 
 ${
+VectorBuffer NavigationMeshGetTileData(const NavigationMesh* navMesh, const IntVector2& tile)
+{
+    VectorBuffer buffer;
+    buffer.SetData(navMesh->GetTileData(tile));
+    return buffer;
+}
+
+bool NavigationMeshAddTile(NavigationMesh* navMesh, const VectorBuffer& tileData)
+{
+    return navMesh->AddTile(tileData.GetBuffer());
+}
+
 const PODVector<Vector3>& NavigationMeshFindPath(NavigationMesh* navMesh, const Vector3& start, const Vector3& end, const Vector3& extents = Vector3::ONE)
 {
     static PODVector<Vector3> dest;

+ 7 - 0
Source/Urho3D/Math/BoundingBox.cpp

@@ -196,6 +196,13 @@ Rect BoundingBox::Projected(const Matrix4& projection) const
     return rect;
 }
 
+float BoundingBox::DistanceToPoint(const Vector3& point) const
+{
+    const Vector3 offset = Center() - point;
+    const Vector3 absOffset(Abs(offset.x_), Abs(offset.y_), Abs(offset.z_));
+    return VectorMax(Vector3::ZERO, absOffset - HalfSize()).Length();
+}
+
 Intersection BoundingBox::IsInside(const Sphere& sphere) const
 {
     float distSquared = 0;

+ 2 - 0
Source/Urho3D/Math/BoundingBox.h

@@ -273,6 +273,8 @@ public:
     BoundingBox Transformed(const Matrix3x4& transform) const;
     /// Return projected by a 4x4 projection matrix.
     Rect Projected(const Matrix4& projection) const;
+    /// Return distance to point.
+    float DistanceToPoint(const Vector3& point) const;
 
     /// Test if a point is inside.
     Intersection IsInside(const Vector3& point) const

+ 13 - 0
Source/Urho3D/Math/MathDefs.h

@@ -205,6 +205,19 @@ inline unsigned NextPowerOfTwo(unsigned value)
     return ++value;
 }
 
+/// Return the largest power of two that is less or equal to the given value.
+inline unsigned IntegerLog2(unsigned value)
+{
+    unsigned ret = 0;
+    while (value > 1)
+    {
+        value >>= 1;
+        ++ret;
+    }
+    return ret;
+
+}
+
 /// Count the number of set bits in a mask.
 inline unsigned CountSetBits(unsigned value)
 {

+ 23 - 11
Source/Urho3D/Navigation/CrowdAgent.cpp

@@ -28,6 +28,7 @@
 #include "../IO/Log.h"
 #include "../IO/MemoryBuffer.h"
 #include "../Navigation/NavigationEvents.h"
+#include "../Navigation/NavigationMesh.h"
 #include "../Navigation/CrowdAgent.h"
 #include "../Scene/Node.h"
 #include "../Scene/Scene.h"
@@ -93,6 +94,7 @@ CrowdAgent::CrowdAgent(Context* context) :
     previousAgentState_(CA_STATE_WALKING),
     ignoreTransformChanges_(false)
 {
+    SubscribeToEvent(E_NAVIGATION_TILE_ADDED, URHO3D_HANDLER(CrowdAgent, HandleNavigationTileAdded));
 }
 
 CrowdAgent::~CrowdAgent()
@@ -626,20 +628,11 @@ void CrowdAgent::OnMarkedDirty(Node* node)
         {
             Vector3& agentPos = reinterpret_cast<Vector3&>(agent->npos);
             Vector3 nodePos = node->GetWorldPosition();
-            
+
             // Only reset position / state if actually changed
             if (nodePos != agentPos)
             {
-                // If position difference is significant, readd to crowd (issue 1695)
-                /// \todo Somewhat arbitrary
-                float diff = (agentPos - nodePos).LengthSquared();
-                if (diff >= 1.0f)
-                {
-                    RemoveAgentFromCrowd();
-                    AddAgentToCrowd();
-                }
-                else
-                    agentPos = nodePos;
+                agentPos = nodePos;
 
                 // If the node has been externally altered, provide the opportunity for DetourCrowd to reevaluate the crowd agent
                 if (agent->state == CA_STATE_INVALID)
@@ -654,4 +647,23 @@ const dtCrowdAgent* CrowdAgent::GetDetourCrowdAgent() const
     return IsInCrowd() ? crowdManager_->GetDetourCrowdAgent(agentCrowdId_) : 0;
 }
 
+void CrowdAgent::HandleNavigationTileAdded(StringHash eventType, VariantMap& eventData)
+{
+    if (!crowdManager_)
+        return;
+
+    NavigationMesh* mesh = static_cast<NavigationMesh*>(eventData[NavigationTileAdded::P_MESH].GetPtr());
+    if (crowdManager_->GetNavigationMesh() != mesh)
+        return;
+
+    const IntVector2 tile = eventData[NavigationTileRemoved::P_TILE].GetIntVector2();
+    const IntVector2 agentTile = mesh->GetTileIndex(node_->GetWorldPosition());
+    const BoundingBox boundingBox = mesh->GetTileBoudningBox(agentTile);
+    if (tile == agentTile && IsInCrowd())
+    {
+        RemoveAgentFromCrowd();
+        AddAgentToCrowd();
+    }
+}
+
 }

+ 2 - 0
Source/Urho3D/Navigation/CrowdAgent.h

@@ -188,6 +188,8 @@ protected:
     virtual void OnMarkedDirty(Node* node);
     /// Get internal Detour crowd agent.
     const dtCrowdAgent* GetDetourCrowdAgent() const;
+    /// Handle navigation mesh tile added.
+    void HandleNavigationTileAdded(StringHash eventType, VariantMap& eventData);
 
 private:
     /// Update Detour crowd agent parameter.

+ 296 - 110
Source/Urho3D/Navigation/DynamicNavigationMesh.cpp

@@ -238,6 +238,105 @@ void DynamicNavigationMesh::RegisterObject(Context* context)
     URHO3D_ACCESSOR_ATTRIBUTE("Draw Obstacles", GetDrawObstacles, SetDrawObstacles, bool, false, AM_DEFAULT);
 }
 
+bool DynamicNavigationMesh::Allocate(const BoundingBox& boundingBox, unsigned maxTiles)
+{
+    // Release existing navigation data and zero the bounding box
+    ReleaseNavigationMesh();
+
+    if (!node_)
+        return false;
+
+    if (!node_->GetWorldScale().Equals(Vector3::ONE))
+        URHO3D_LOGWARNING("Navigation mesh root node has scaling. Agent parameters may not work as intended");
+
+    boundingBox_ = boundingBox.Transformed(node_->GetWorldTransform().Inverse());
+    maxTiles = NextPowerOfTwo(maxTiles);
+
+    // Calculate number of tiles
+    int gridW = 0, gridH = 0;
+    float tileEdgeLength = (float)tileSize_ * cellSize_;
+    rcCalcGridSize(&boundingBox_.min_.x_, &boundingBox_.max_.x_, cellSize_, &gridW, &gridH);
+    numTilesX_ = (gridW + tileSize_ - 1) / tileSize_;
+    numTilesZ_ = (gridH + tileSize_ - 1) / tileSize_;
+
+    // Calculate max number of polygons, 22 bits available to identify both tile & polygon within tile
+    unsigned tileBits = IntegerLog2(maxTiles);
+    unsigned maxPolys = (unsigned)(1 << (22 - tileBits));
+
+    dtNavMeshParams params;
+    rcVcopy(params.orig, &boundingBox_.min_.x_);
+    params.tileWidth = tileEdgeLength;
+    params.tileHeight = tileEdgeLength;
+    params.maxTiles = maxTiles;
+    params.maxPolys = maxPolys;
+
+    navMesh_ = dtAllocNavMesh();
+    if (!navMesh_)
+    {
+        URHO3D_LOGERROR("Could not allocate navigation mesh");
+        return false;
+    }
+
+    if (dtStatusFailed(navMesh_->init(&params)))
+    {
+        URHO3D_LOGERROR("Could not initialize navigation mesh");
+        ReleaseNavigationMesh();
+        return false;
+    }
+
+    dtTileCacheParams tileCacheParams;
+    memset(&tileCacheParams, 0, sizeof(tileCacheParams));
+    rcVcopy(tileCacheParams.orig, &boundingBox_.min_.x_);
+    tileCacheParams.ch = cellHeight_;
+    tileCacheParams.cs = cellSize_;
+    tileCacheParams.width = tileSize_;
+    tileCacheParams.height = tileSize_;
+    tileCacheParams.maxSimplificationError = edgeMaxError_;
+    tileCacheParams.maxTiles = maxTiles * maxLayers_;
+    tileCacheParams.maxObstacles = maxObstacles_;
+    // Settings from NavigationMesh
+    tileCacheParams.walkableClimb = agentMaxClimb_;
+    tileCacheParams.walkableHeight = agentHeight_;
+    tileCacheParams.walkableRadius = agentRadius_;
+
+    tileCache_ = dtAllocTileCache();
+    if (!tileCache_)
+    {
+        URHO3D_LOGERROR("Could not allocate tile cache");
+        ReleaseNavigationMesh();
+        return false;
+    }
+
+    if (dtStatusFailed(tileCache_->init(&tileCacheParams, allocator_.Get(), compressor_.Get(), meshProcessor_.Get())))
+    {
+        URHO3D_LOGERROR("Could not initialize tile cache");
+        ReleaseNavigationMesh();
+        return false;
+    }
+
+    URHO3D_LOGDEBUG("Allocated empty navigation mesh with max " + String(maxTiles) + " tiles");
+
+    // Scan for obstacles to insert into us
+    PODVector<Node*> obstacles;
+    GetScene()->GetChildrenWithComponent<Obstacle>(obstacles, true);
+    for (unsigned i = 0; i < obstacles.Size(); ++i)
+    {
+        Obstacle* obs = obstacles[i]->GetComponent<Obstacle>();
+        if (obs && obs->IsEnabledEffective())
+            AddObstacle(obs);
+    }
+
+    // Send a notification event to concerned parties that we've been fully rebuilt
+    {
+        using namespace NavigationMeshRebuilt;
+        VariantMap& buildEventParams = GetContext()->GetEventDataMap();
+        buildEventParams[P_NODE] = node_;
+        buildEventParams[P_MESH] = this;
+        SendEvent(E_NAVIGATION_MESH_REBUILT, buildEventParams);
+    }
+    return true;
+}
+
 bool DynamicNavigationMesh::Build()
 {
     URHO3D_PROFILE(BuildNavigationMesh);
@@ -276,14 +375,7 @@ bool DynamicNavigationMesh::Build()
 
         // Calculate max. number of tiles and polygons, 22 bits available to identify both tile & polygon within tile
         unsigned maxTiles = NextPowerOfTwo((unsigned)(numTilesX_ * numTilesZ_)) * maxLayers_;
-        unsigned tileBits = 0;
-        unsigned temp = maxTiles;
-        while (temp > 1)
-        {
-            temp >>= 1;
-            ++tileBits;
-        }
-
+        unsigned tileBits = IntegerLog2(maxTiles);
         unsigned maxPolys = (unsigned)(1 << (22 - tileBits));
 
         dtNavMeshParams params;
@@ -356,10 +448,9 @@ bool DynamicNavigationMesh::Build()
                         tiles[i].data = 0x0;
                     }
                 }
+                tileCache_->buildNavMeshTilesAt(x, z, navMesh_);
                 ++numTiles;
             }
-            for (int x = 0; x < numTilesX_; ++x)
-                tileCache_->buildNavMeshTilesAt(x, z, navMesh_);
         }
 
         // For a full build it's necessary to update the nav mesh
@@ -419,45 +510,87 @@ bool DynamicNavigationMesh::Build(const BoundingBox& boundingBox)
     int ex = Clamp((int)((localSpaceBox.max_.x_ - boundingBox_.min_.x_) / tileEdgeLength), 0, numTilesX_ - 1);
     int ez = Clamp((int)((localSpaceBox.max_.z_ - boundingBox_.min_.z_) / tileEdgeLength), 0, numTilesZ_ - 1);
 
-    unsigned numTiles = 0;
+    unsigned numTiles = BuildTiles(geometryList, IntVector2(sx, sz), IntVector2(ex, ez));
 
-    for (int z = sz; z <= ez; ++z)
-    {
-        for (int x = sx; x <= ex; ++x)
-        {
-            dtCompressedTileRef existing[TILECACHE_MAXLAYERS];
-            const int existingCt = tileCache_->getTilesAt(x, z, existing, maxLayers_);
-            for (int i = 0; i < existingCt; ++i)
-            {
-                unsigned char* data = 0x0;
-                if (!dtStatusFailed(tileCache_->removeTile(existing[i], &data, 0)) && data != 0x0)
-                    dtFree(data);
-            }
+    URHO3D_LOGDEBUG("Rebuilt " + String(numTiles) + " tiles of the navigation mesh");
+    return true;
+}
 
-            TileCacheData tiles[TILECACHE_MAXLAYERS];
-            int layerCt = BuildTile(geometryList, x, z, tiles);
-            for (int i = 0; i < layerCt; ++i)
-            {
-                dtCompressedTileRef tileRef;
-                int status = tileCache_->addTile(tiles[i].data, tiles[i].dataSize, DT_COMPRESSEDTILE_FREE_DATA, &tileRef);
-                if (dtStatusFailed((dtStatus)status))
-                {
-                    dtFree(tiles[i].data);
-                    tiles[i].data = 0x0;
-                }
-                else
-                {
-                    tileCache_->buildNavMeshTile(tileRef, navMesh_);
-                    ++numTiles;
-                }
-            }
-        }
+bool DynamicNavigationMesh::Build(const IntVector2& from, const IntVector2& to)
+{
+    URHO3D_PROFILE(BuildPartialNavigationMesh);
+
+    if (!node_)
+        return false;
+
+    if (!navMesh_)
+    {
+        URHO3D_LOGERROR("Navigation mesh must first be built fully before it can be partially rebuilt");
+        return false;
     }
 
+    if (!node_->GetWorldScale().Equals(Vector3::ONE))
+        URHO3D_LOGWARNING("Navigation mesh root node has scaling. Agent parameters may not work as intended");
+
+    Vector<NavigationGeometryInfo> geometryList;
+    CollectGeometries(geometryList);
+
+    unsigned numTiles = BuildTiles(geometryList, from, to);
+
     URHO3D_LOGDEBUG("Rebuilt " + String(numTiles) + " tiles of the navigation mesh");
     return true;
 }
 
+PODVector<unsigned char> DynamicNavigationMesh::GetTileData(const IntVector2& tile) const
+{
+    VectorBuffer ret;
+    WriteTiles(ret, tile.x_, tile.y_);
+    return ret.GetBuffer();
+}
+
+bool DynamicNavigationMesh::IsObstacleInTile(Obstacle* obstacle, const IntVector2& tile) const
+{
+    const BoundingBox tileBoundingBox = GetTileBoudningBox(tile);
+    const Vector3 obstaclePosition = obstacle->GetNode()->GetWorldPosition();
+    return tileBoundingBox.DistanceToPoint(obstaclePosition) < obstacle->GetRadius();
+}
+
+bool DynamicNavigationMesh::AddTile(const PODVector<unsigned char>& tileData)
+{
+    MemoryBuffer buffer(tileData);
+    return ReadTiles(buffer, false);
+}
+
+void DynamicNavigationMesh::RemoveTile(const IntVector2& tile)
+{
+    if (!navMesh_)
+        return;
+
+    dtCompressedTileRef existing[TILECACHE_MAXLAYERS];
+    const int existingCt = tileCache_->getTilesAt(tile.x_, tile.y_, existing, maxLayers_);
+    for (int i = 0; i < existingCt; ++i)
+    {
+        unsigned char* data = 0x0;
+        if (!dtStatusFailed(tileCache_->removeTile(existing[i], &data, 0)) && data != 0x0)
+            dtFree(data);
+    }
+
+    NavigationMesh::RemoveTile(tile);
+}
+
+void DynamicNavigationMesh::RemoveAllTiles()
+{
+    int numTiles = tileCache_->getTileCount();
+    for (int i = 0; i < numTiles; ++i)
+    {
+        const dtCompressedTile* tile = tileCache_->getTile(i);
+        assert(tile);
+        if (tile->header)
+            tileCache_->removeTile(tileCache_->getTileRef(tile), 0, 0);
+    }
+
+    NavigationMesh::RemoveAllTiles();
+}
 
 void DynamicNavigationMesh::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
 {
@@ -468,29 +601,21 @@ void DynamicNavigationMesh::DrawDebugGeometry(DebugRenderer* debug, bool depthTe
 
     const dtNavMesh* navMesh = navMesh_;
 
-    for (int z = 0; z < numTilesZ_; ++z)
+    for (int j = 0; j < navMesh->getMaxTiles(); ++j)
     {
-        for (int x = 0; x < numTilesX_; ++x)
+        const dtMeshTile* tile = navMesh->getTile(j);
+        assert(tile);
+        if (!tile->header)
+            continue;
+
+        for (int i = 0; i < tile->header->polyCount; ++i)
         {
-            // Get the layers from the tile-cache
-            const dtMeshTile* tiles[TILECACHE_MAXLAYERS];
-            int tileCount = navMesh->getTilesAt(x, z, tiles, TILECACHE_MAXLAYERS);
-            for (int i = 0; i < tileCount; ++i)
+            dtPoly* poly = tile->polys + i;
+            for (unsigned j = 0; j < poly->vertCount; ++j)
             {
-                const dtMeshTile* tile = tiles[i];
-                if (!tile)
-                    continue;
-
-                for (int i = 0; i < tile->header->polyCount; ++i)
-                {
-                    dtPoly* poly = tile->polys + i;
-                    for (unsigned j = 0; j < poly->vertCount; ++j)
-                    {
-                        debug->AddLine(worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[j] * 3]),
-                            worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[(j + 1) % poly->vertCount] * 3]),
-                            Color::YELLOW, depthTest);
-                    }
-                }
+                debug->AddLine(worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[j] * 3]),
+                    worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[(j + 1) % poly->vertCount] * 3]),
+                    Color::YELLOW, depthTest);
             }
         }
     }
@@ -596,29 +721,8 @@ void DynamicNavigationMesh::SetNavigationDataAttr(const PODVector<unsigned char>
         return;
     }
 
-    while (!buffer.IsEof())
-    {
-        dtTileCacheLayerHeader header;
-        buffer.Read(&header, sizeof(dtTileCacheLayerHeader));
-        const int dataSize = buffer.ReadInt();
-        unsigned char* data = (unsigned char*)dtAlloc(dataSize, DT_ALLOC_PERM);
-        buffer.Read(data, (unsigned)dataSize);
-
-        if (dtStatusFailed(tileCache_->addTile(data, dataSize, DT_TILE_FREE_DATA, 0)))
-        {
-            URHO3D_LOGERROR("Failed to add tile");
-            dtFree(data);
-            return;
-        }
-    }
-
-    for (int x = 0; x < numTilesX_; ++x)
-    {
-        for (int z = 0; z < numTilesZ_; ++z)
-            tileCache_->buildNavMeshTilesAt(x, z, navMesh_);
-    }
-
-    tileCache_->update(0, navMesh_);
+    ReadTiles(buffer, true);
+    // \todo Shall we send E_NAVIGATION_MESH_REBUILT here?
 }
 
 PODVector<unsigned char> DynamicNavigationMesh::GetNavigationDataAttr() const
@@ -637,23 +741,8 @@ PODVector<unsigned char> DynamicNavigationMesh::GetNavigationDataAttr() const
         ret.Write(tcParams, sizeof(dtTileCacheParams));
 
         for (int z = 0; z < numTilesZ_; ++z)
-        {
             for (int x = 0; x < numTilesX_; ++x)
-            {
-                dtCompressedTileRef tiles[TILECACHE_MAXLAYERS];
-                const int ct = tileCache_->getTilesAt(x, z, tiles, maxLayers_);
-                for (int i = 0; i < ct; ++i)
-                {
-                    const dtCompressedTile* tile = tileCache_->getTileByRef(tiles[i]);
-                    if (!tile || !tile->header || !tile->dataSize)
-                        continue; // Don't write "void-space" tiles
-                    // The header conveniently has the majority of the information required
-                    ret.Write(tile->header, sizeof(dtTileCacheLayerHeader));
-                    ret.WriteInt(tile->dataSize);
-                    ret.Write(tile->data, (unsigned)tile->dataSize);
-                }
-            }
-        }
+                WriteTiles(ret, x, z);
     }
     return ret.GetBuffer();
 }
@@ -665,22 +754,79 @@ void DynamicNavigationMesh::SetMaxLayers(unsigned maxLayers)
     maxLayers_ = Max(3U, Min(maxLayers, TILECACHE_MAXLAYERS));
 }
 
+void DynamicNavigationMesh::WriteTiles(Serializer& dest, int x, int z) const
+{
+    dtCompressedTileRef tiles[TILECACHE_MAXLAYERS];
+    const int ct = tileCache_->getTilesAt(x, z, tiles, maxLayers_);
+    for (int i = 0; i < ct; ++i)
+    {
+        const dtCompressedTile* tile = tileCache_->getTileByRef(tiles[i]);
+        if (!tile || !tile->header || !tile->dataSize)
+            continue; // Don't write "void-space" tiles
+                      // The header conveniently has the majority of the information required
+        dest.Write(tile->header, sizeof(dtTileCacheLayerHeader));
+        dest.WriteInt(tile->dataSize);
+        dest.Write(tile->data, (unsigned)tile->dataSize);
+    }
+}
+
+bool DynamicNavigationMesh::ReadTiles(Deserializer& source, bool silent)
+{
+    tileQueue_.Clear();
+    while (!source.IsEof())
+    {
+        dtTileCacheLayerHeader header;
+        source.Read(&header, sizeof(dtTileCacheLayerHeader));
+        const int dataSize = source.ReadInt();
+
+        unsigned char* data = (unsigned char*)dtAlloc(dataSize, DT_ALLOC_PERM);
+        if (!data)
+        {
+            URHO3D_LOGERROR("Could not allocate data for navigation mesh tile");
+            return false;
+        }
+
+        source.Read(data, (unsigned)dataSize);
+        if (dtStatusFailed(tileCache_->addTile(data, dataSize, DT_TILE_FREE_DATA, 0)))
+        {
+            URHO3D_LOGERROR("Failed to add tile");
+            dtFree(data);
+            return false;
+        }
+
+        const IntVector2 tileIdx = IntVector2(header.tx, header.ty);
+        if (tileQueue_.Empty() || tileQueue_.Back() != tileIdx)
+            tileQueue_.Push(tileIdx);
+    }
+
+    for (unsigned i = 0; i < tileQueue_.Size(); ++i)
+        tileCache_->buildNavMeshTilesAt(tileQueue_[i].x_, tileQueue_[i].y_, navMesh_);
+
+    tileCache_->update(0, navMesh_);
+
+    // Send event
+    if (!silent)
+    {
+        for (unsigned i = 0; i < tileQueue_.Size(); ++i)
+        {
+            using namespace NavigationTileAdded;
+            VariantMap& eventData = GetContext()->GetEventDataMap();
+            eventData[P_NODE] = GetNode();
+            eventData[P_MESH] = this;
+            eventData[P_TILE] = tileQueue_[i];
+            SendEvent(E_NAVIGATION_TILE_ADDED, eventData);
+        }
+    }
+    return true;
+}
+
 int DynamicNavigationMesh::BuildTile(Vector<NavigationGeometryInfo>& geometryList, int x, int z, TileCacheData* tiles)
 {
     URHO3D_PROFILE(BuildNavigationMeshTile);
 
     tileCache_->removeTile(navMesh_->getTileRefAt(x, z, 0), 0, 0);
 
-    float tileEdgeLength = (float)tileSize_ * cellSize_;
-
-    BoundingBox tileBoundingBox(Vector3(
-            boundingBox_.min_.x_ + tileEdgeLength * (float)x,
-            boundingBox_.min_.y_,
-            boundingBox_.min_.z_ + tileEdgeLength * (float)z),
-        Vector3(
-            boundingBox_.min_.x_ + tileEdgeLength * (float)(x + 1),
-            boundingBox_.max_.y_,
-            boundingBox_.min_.z_ + tileEdgeLength * (float)(z + 1)));
+    const BoundingBox tileBoundingBox = GetTileBoudningBox(IntVector2(x, z));
 
     DynamicNavBuildData build(allocator_.Get());
 
@@ -853,6 +999,46 @@ int DynamicNavigationMesh::BuildTile(Vector<NavigationGeometryInfo>& geometryLis
     return retCt;
 }
 
+unsigned DynamicNavigationMesh::BuildTiles(Vector<NavigationGeometryInfo>& geometryList, const IntVector2& from, const IntVector2& to)
+{
+    unsigned numTiles = 0;
+
+    for (int z = from.y_; z <= to.y_; ++z)
+    {
+        for (int x = from.x_; x <= to.x_; ++x)
+        {
+            dtCompressedTileRef existing[TILECACHE_MAXLAYERS];
+            const int existingCt = tileCache_->getTilesAt(x, z, existing, maxLayers_);
+            for (int i = 0; i < existingCt; ++i)
+            {
+                unsigned char* data = 0x0;
+                if (!dtStatusFailed(tileCache_->removeTile(existing[i], &data, 0)) && data != 0x0)
+                    dtFree(data);
+            }
+
+            TileCacheData tiles[TILECACHE_MAXLAYERS];
+            int layerCt = BuildTile(geometryList, x, z, tiles);
+            for (int i = 0; i < layerCt; ++i)
+            {
+                dtCompressedTileRef tileRef;
+                int status = tileCache_->addTile(tiles[i].data, tiles[i].dataSize, DT_COMPRESSEDTILE_FREE_DATA, &tileRef);
+                if (dtStatusFailed((dtStatus)status))
+                {
+                    dtFree(tiles[i].data);
+                    tiles[i].data = 0x0;
+                }
+                else
+                {
+                    tileCache_->buildNavMeshTile(tileRef, navMesh_);
+                    ++numTiles;
+                }
+            }
+        }
+    }
+
+    return numTiles;
+}
+
 PODVector<OffMeshConnection*> DynamicNavigationMesh::CollectOffMeshConnections(const BoundingBox& bounds)
 {
     PODVector<OffMeshConnection*> connections;

+ 23 - 1
Source/Urho3D/Navigation/DynamicNavigationMesh.h

@@ -54,10 +54,24 @@ public:
     /// Register with engine context.
     static void RegisterObject(Context*);
 
+    /// Allocate the navigation mesh without building any tiles. Bounding box is not padded. Return true if successful.
+    virtual bool Allocate(const BoundingBox& boundingBox, unsigned maxTiles);
     /// Build/rebuild the entire navigation mesh.
     virtual bool Build();
     /// Build/rebuild a portion of the navigation mesh.
     virtual bool Build(const BoundingBox& boundingBox);
+    /// Rebuild part of the navigation mesh in the rectangular area. Return true if successful.
+    virtual bool Build(const IntVector2& from, const IntVector2& to);
+    /// Return tile data.
+    virtual PODVector<unsigned char> GetTileData(const IntVector2& tile) const;
+    /// Return whether the Obstacle is touching the given tile.
+    bool IsObstacleInTile(Obstacle* obstacle, const IntVector2& tile) const;
+    /// Add tile to navigation mesh.
+    virtual bool AddTile(const PODVector<unsigned char>& tileData);
+    /// Remove tile from navigation mesh.
+    virtual void RemoveTile(const IntVector2& tile);
+    /// Remove all tiles from navigation mesh.
+    virtual void RemoveAllTiles();
     /// Visualize the component as debug geometry.
     virtual void DrawDebugGeometry(DebugRenderer* debug, bool depthTest);
     /// Add debug geometry to the debug renderer.
@@ -100,13 +114,19 @@ protected:
     void RemoveObstacle(Obstacle*, bool silent = false);
 
     /// Build one tile of the navigation mesh. Return true if successful.
-    int BuildTile(Vector<NavigationGeometryInfo>& geometryList, int x, int z, TileCacheData*);
+    int BuildTile(Vector<NavigationGeometryInfo>& geometryList, int x, int z, TileCacheData* tiles);
+    /// Build tiles in the rectangular area. Return number of built tiles.
+    unsigned BuildTiles(Vector<NavigationGeometryInfo>& geometryList, const IntVector2& from, const IntVector2& to);
     /// Off-mesh connections to be rebuilt in the mesh processor.
     PODVector<OffMeshConnection*> CollectOffMeshConnections(const BoundingBox& bounds);
     /// Release the navigation mesh, query, and tile cache.
     virtual void ReleaseNavigationMesh();
 
 private:
+    /// Write tiles data.
+    void WriteTiles(Serializer& dest, int x, int z) const;
+    /// Read tiles data to the navigation mesh.
+    bool ReadTiles(Deserializer& source, bool silent);
     /// Free the tile cache.
     void ReleaseTileCache();
 
@@ -124,6 +144,8 @@ private:
     unsigned maxLayers_;
     /// Debug draw Obstacles.
     bool drawObstacles_;
+    /// Queue of tiles to be built.
+    PODVector<IntVector2> tileQueue_;
 };
 
 }

+ 23 - 0
Source/Urho3D/Navigation/NavigationEvents.h

@@ -43,6 +43,29 @@ URHO3D_EVENT(E_NAVIGATION_AREA_REBUILT, NavigationAreaRebuilt)
     URHO3D_PARAM(P_BOUNDSMAX, BoundsMax); // Vector3
 }
 
+/// Mesh tile is added to navigation mesh.
+URHO3D_EVENT(E_NAVIGATION_TILE_ADDED, NavigationTileAdded)
+{
+    URHO3D_PARAM(P_NODE, Node); // Node pointer
+    URHO3D_PARAM(P_MESH, Mesh); // NavigationMesh pointer
+    URHO3D_PARAM(P_TILE, Tile); // IntVector2
+}
+
+/// Mesh tile is removed from navigation mesh.
+URHO3D_EVENT(E_NAVIGATION_TILE_REMOVED, NavigationTileRemoved)
+{
+    URHO3D_PARAM(P_NODE, Node); // Node pointer
+    URHO3D_PARAM(P_MESH, Mesh); // NavigationMesh pointer
+    URHO3D_PARAM(P_TILE, Tile); // IntVector2
+}
+
+/// All mesh tiles are removed from navigation mesh.
+URHO3D_EVENT(E_NAVIGATION_ALL_TILES_REMOVED, NavigationAllTilesRemoved)
+{
+    URHO3D_PARAM(P_NODE, Node); // Node pointer
+    URHO3D_PARAM(P_MESH, Mesh); // NavigationMesh pointer
+}
+
 /// Crowd agent formation.
 URHO3D_EVENT(E_CROWD_AGENT_FORMATION, CrowdAgentFormation)
 {

+ 252 - 90
Source/Urho3D/Navigation/NavigationMesh.cpp

@@ -168,29 +168,24 @@ void NavigationMesh::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
 
     const dtNavMesh* navMesh = navMesh_;
 
-    for (int z = 0; z < numTilesZ_; ++z)
+    for (int j = 0; j < navMesh->getMaxTiles(); ++j)
     {
-        for (int x = 0; x < numTilesX_; ++x)
+        const dtMeshTile* tile = navMesh->getTile(j);
+        assert(tile);
+        if (!tile->header)
+            continue;
+
+        for (int i = 0; i < tile->header->polyCount; ++i)
         {
-            for (int i = 0; i < 128; ++i)
+            dtPoly* poly = tile->polys + i;
+            for (unsigned j = 0; j < poly->vertCount; ++j)
             {
-                const dtMeshTile* tile = navMesh->getTileAt(x, z, i);
-                if (!tile)
-                    continue;
-
-                for (int i = 0; i < tile->header->polyCount; ++i)
-                {
-                    dtPoly* poly = tile->polys + i;
-                    for (unsigned j = 0; j < poly->vertCount; ++j)
-                    {
-                        debug->AddLine(
-                            worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[j] * 3]),
-                            worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[(j + 1) % poly->vertCount] * 3]),
-                            Color::YELLOW,
-                            depthTest
-                        );
-                    }
-                }
+                debug->AddLine(
+                    worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[j] * 3]),
+                    worldTransform * *reinterpret_cast<const Vector3*>(&tile->verts[poly->verts[(j + 1) % poly->vertCount] * 3]),
+                    Color::YELLOW,
+                    depthTest
+                );
             }
         }
     }
@@ -327,6 +322,65 @@ void NavigationMesh::SetPadding(const Vector3& padding)
     MarkNetworkUpdate();
 }
 
+bool NavigationMesh::Allocate(const BoundingBox& boundingBox, unsigned maxTiles)
+{
+    // Release existing navigation data and zero the bounding box
+    ReleaseNavigationMesh();
+
+    if (!node_)
+        return false;
+
+    if (!node_->GetWorldScale().Equals(Vector3::ONE))
+        URHO3D_LOGWARNING("Navigation mesh root node has scaling. Agent parameters may not work as intended");
+
+    boundingBox_ = boundingBox.Transformed(node_->GetWorldTransform().Inverse());
+    maxTiles = NextPowerOfTwo(maxTiles);
+
+    // Calculate number of tiles
+    int gridW = 0, gridH = 0;
+    float tileEdgeLength = (float)tileSize_ * cellSize_;
+    rcCalcGridSize(&boundingBox_.min_.x_, &boundingBox_.max_.x_, cellSize_, &gridW, &gridH);
+    numTilesX_ = (gridW + tileSize_ - 1) / tileSize_;
+    numTilesZ_ = (gridH + tileSize_ - 1) / tileSize_;
+
+    // Calculate max number of polygons, 22 bits available to identify both tile & polygon within tile
+    unsigned tileBits = IntegerLog2(maxTiles);
+    unsigned maxPolys = (unsigned)(1 << (22 - tileBits));
+
+    dtNavMeshParams params;
+    rcVcopy(params.orig, &boundingBox_.min_.x_);
+    params.tileWidth = tileEdgeLength;
+    params.tileHeight = tileEdgeLength;
+    params.maxTiles = maxTiles;
+    params.maxPolys = maxPolys;
+
+    navMesh_ = dtAllocNavMesh();
+    if (!navMesh_)
+    {
+        URHO3D_LOGERROR("Could not allocate navigation mesh");
+        return false;
+    }
+
+    if (dtStatusFailed(navMesh_->init(&params)))
+    {
+        URHO3D_LOGERROR("Could not initialize navigation mesh");
+        ReleaseNavigationMesh();
+        return false;
+    }
+
+    URHO3D_LOGDEBUG("Allocated empty navigation mesh with max " + String(maxTiles) + " tiles");
+
+    // Send a notification event to concerned parties that we've been fully rebuilt
+    {
+        using namespace NavigationMeshRebuilt;
+        VariantMap& buildEventParams = GetContext()->GetEventDataMap();
+        buildEventParams[P_NODE] = node_;
+        buildEventParams[P_MESH] = this;
+        SendEvent(E_NAVIGATION_MESH_REBUILT, buildEventParams);
+    }
+    return true;
+}
+
 bool NavigationMesh::Build()
 {
     URHO3D_PROFILE(BuildNavigationMesh);
@@ -366,14 +420,7 @@ bool NavigationMesh::Build()
 
         // Calculate max. number of tiles and polygons, 22 bits available to identify both tile & polygon within tile
         unsigned maxTiles = NextPowerOfTwo((unsigned)(numTilesX_ * numTilesZ_));
-        unsigned tileBits = 0;
-        unsigned temp = maxTiles;
-        while (temp > 1)
-        {
-            temp >>= 1;
-            ++tileBits;
-        }
-
+        unsigned tileBits = IntegerLog2(maxTiles);
         unsigned maxPolys = (unsigned)(1 << (22 - tileBits));
 
         dtNavMeshParams params;
@@ -398,16 +445,7 @@ bool NavigationMesh::Build()
         }
 
         // Build each tile
-        unsigned numTiles = 0;
-
-        for (int z = 0; z < numTilesZ_; ++z)
-        {
-            for (int x = 0; x < numTilesX_; ++x)
-            {
-                if (BuildTile(geometryList, x, z))
-                    ++numTiles;
-            }
-        }
+        unsigned numTiles = BuildTiles(geometryList, IntVector2::ZERO, GetNumTiles() - IntVector2::ONE);
 
         URHO3D_LOGDEBUG("Built navigation mesh with " + String(numTiles) + " tiles");
 
@@ -452,21 +490,120 @@ bool NavigationMesh::Build(const BoundingBox& boundingBox)
     int ex = Clamp((int)((localSpaceBox.max_.x_ - boundingBox_.min_.x_) / tileEdgeLength), 0, numTilesX_ - 1);
     int ez = Clamp((int)((localSpaceBox.max_.z_ - boundingBox_.min_.z_) / tileEdgeLength), 0, numTilesZ_ - 1);
 
-    unsigned numTiles = 0;
+    unsigned numTiles = BuildTiles(geometryList, IntVector2(sx, sz), IntVector2(ex, ez));
 
-    for (int z = sz; z <= ez; ++z)
+    URHO3D_LOGDEBUG("Rebuilt " + String(numTiles) + " tiles of the navigation mesh");
+    return true;
+}
+
+bool NavigationMesh::Build(const IntVector2& from, const IntVector2& to)
+{
+    URHO3D_PROFILE(BuildPartialNavigationMesh);
+
+    if (!node_)
+        return false;
+
+    if (!navMesh_)
     {
-        for (int x = sx; x <= ex; ++x)
-        {
-            if (BuildTile(geometryList, x, z))
-                ++numTiles;
-        }
+        URHO3D_LOGERROR("Navigation mesh must first be built fully before it can be partially rebuilt");
+        return false;
     }
 
+    if (!node_->GetWorldScale().Equals(Vector3::ONE))
+        URHO3D_LOGWARNING("Navigation mesh root node has scaling. Agent parameters may not work as intended");
+
+    Vector<NavigationGeometryInfo> geometryList;
+    CollectGeometries(geometryList);
+
+    unsigned numTiles = BuildTiles(geometryList, from, to);
+
     URHO3D_LOGDEBUG("Rebuilt " + String(numTiles) + " tiles of the navigation mesh");
     return true;
 }
 
+PODVector<unsigned char> NavigationMesh::GetTileData(const IntVector2& tile) const
+{
+    VectorBuffer ret;
+    WriteTile(ret, tile.x_, tile.y_);
+    return ret.GetBuffer();
+}
+
+bool NavigationMesh::AddTile(const PODVector<unsigned char>& tileData)
+{
+    MemoryBuffer buffer(tileData);
+    return ReadTile(buffer, false);
+}
+
+bool NavigationMesh::HasTile(const IntVector2& tile) const
+{
+    if (navMesh_)
+        return !!navMesh_->getTileAt(tile.x_, tile.y_, 0);
+    return false;
+}
+
+BoundingBox NavigationMesh::GetTileBoudningBox(const IntVector2& tile) const
+{
+    const float tileEdgeLength = (float)tileSize_ * cellSize_;
+    return BoundingBox(
+        Vector3(
+            boundingBox_.min_.x_ + tileEdgeLength * (float)tile.x_,
+            boundingBox_.min_.y_,
+            boundingBox_.min_.z_ + tileEdgeLength * (float)tile.y_
+        ),
+        Vector3(
+            boundingBox_.min_.x_ + tileEdgeLength * (float)(tile.x_ + 1),
+            boundingBox_.max_.y_,
+            boundingBox_.min_.z_ + tileEdgeLength * (float)(tile.y_ + 1)
+        ));
+}
+
+IntVector2 NavigationMesh::GetTileIndex(const Vector3& position) const
+{
+    const float tileEdgeLength = (float)tileSize_ * cellSize_;
+    const Vector3 localPosition = node_->GetWorldTransform().Inverse() * position - boundingBox_.min_;
+    const Vector2 localPosition2D(localPosition.x_, localPosition.z_);
+    return VectorMin(VectorMax(IntVector2::ZERO, VectorFloorToInt(localPosition2D / tileEdgeLength)), GetNumTiles() - IntVector2::ONE);
+}
+
+void NavigationMesh::RemoveTile(const IntVector2& tile)
+{
+    if (!navMesh_)
+        return;
+
+    const dtTileRef tileRef = navMesh_->getTileRefAt(tile.x_, tile.y_, 0);
+    if (!tileRef)
+        return;
+
+    navMesh_->removeTile(tileRef, 0, 0);
+
+    // Send event
+    using namespace NavigationTileRemoved;
+    VariantMap& eventData = GetContext()->GetEventDataMap();
+    eventData[P_NODE] = GetNode();
+    eventData[P_MESH] = this;
+    eventData[P_TILE] = tile;
+    SendEvent(E_NAVIGATION_TILE_REMOVED, eventData);
+}
+
+void NavigationMesh::RemoveAllTiles()
+{
+    const dtNavMesh* navMesh = navMesh_;
+    for (int i = 0; i < navMesh_->getMaxTiles(); ++i)
+    {
+        const dtMeshTile* tile = navMesh->getTile(i);
+        assert(tile);
+        if (tile->header)
+            navMesh_->removeTile(navMesh_->getTileRef(tile), 0, 0);
+    }
+
+    // Send event
+    using namespace NavigationAllTilesRemoved;
+    VariantMap& eventData = GetContext()->GetEventDataMap();
+    eventData[P_NODE] = GetNode();
+    eventData[P_MESH] = this;
+    SendEvent(E_NAVIGATION_ALL_TILES_REMOVED, eventData);
+}
+
 Vector3 NavigationMesh::FindNearestPoint(const Vector3& point, const Vector3& extents, const dtQueryFilter* filter,
     dtPolyRef* nearestRef)
 {
@@ -777,30 +914,14 @@ void NavigationMesh::SetNavigationDataAttr(const PODVector<unsigned char>& value
 
     while (!buffer.IsEof())
     {
-        /*int x =*/ buffer.ReadInt();
-        /*int z =*/ buffer.ReadInt();
-        /*dtTileRef tileRef =*/ buffer.ReadUInt();
-        unsigned navDataSize = buffer.ReadUInt();
-
-        unsigned char* navData = (unsigned char*)dtAlloc(navDataSize, DT_ALLOC_PERM);
-        if (!navData)
-        {
-            URHO3D_LOGERROR("Could not allocate data for navigation mesh tile");
-            return;
-        }
-
-        buffer.Read(navData, navDataSize);
-        if (dtStatusFailed(navMesh_->addTile(navData, navDataSize, DT_TILE_FREE_DATA, 0, 0)))
-        {
-            URHO3D_LOGERROR("Failed to add navigation mesh tile");
-            dtFree(navData);
-            return;
-        }
-        else
+        if (ReadTile(buffer, true))
             ++numTiles;
+        else
+            return;
     }
 
     URHO3D_LOGDEBUG("Created navigation mesh with " + String(numTiles) + " tiles from serialized data");
+    // \todo Shall we send E_NAVIGATION_MESH_REBUILT here?
 }
 
 PODVector<unsigned char> NavigationMesh::GetNavigationDataAttr() const
@@ -822,20 +943,8 @@ PODVector<unsigned char> NavigationMesh::GetNavigationDataAttr() const
         const dtNavMesh* navMesh = navMesh_;
 
         for (int z = 0; z < numTilesZ_; ++z)
-        {
             for (int x = 0; x < numTilesX_; ++x)
-            {
-                const dtMeshTile* tile = navMesh->getTileAt(x, z, 0);
-                if (!tile)
-                    continue;
-
-                ret.WriteInt(x);
-                ret.WriteInt(z);
-                ret.WriteUInt(navMesh->getTileRef(tile));
-                ret.WriteUInt((unsigned)tile->dataSize);
-                ret.Write(tile->data, (unsigned)tile->dataSize);
-            }
-        }
+                WriteTile(ret, x, z);
     }
 
     return ret.GetBuffer();
@@ -1141,6 +1250,55 @@ void NavigationMesh::AddTriMeshGeometry(NavBuildData* build, Geometry* geometry,
     }
 }
 
+void NavigationMesh::WriteTile(Serializer& dest, int x, int z) const
+{
+    const dtNavMesh* navMesh = navMesh_;
+    const dtMeshTile* tile = navMesh->getTileAt(x, z, 0);
+    if (!tile)
+        return;
+
+    dest.WriteInt(x);
+    dest.WriteInt(z);
+    dest.WriteUInt(navMesh->getTileRef(tile));
+    dest.WriteUInt((unsigned)tile->dataSize);
+    dest.Write(tile->data, (unsigned)tile->dataSize);
+}
+
+bool NavigationMesh::ReadTile(Deserializer& source, bool silent)
+{
+    const int x = source.ReadInt();
+    const int z = source.ReadInt();
+    /*dtTileRef tileRef =*/ source.ReadUInt();
+    unsigned navDataSize = source.ReadUInt();
+
+    unsigned char* navData = (unsigned char*)dtAlloc(navDataSize, DT_ALLOC_PERM);
+    if (!navData)
+    {
+        URHO3D_LOGERROR("Could not allocate data for navigation mesh tile");
+        return false;
+    }
+
+    source.Read(navData, navDataSize);
+    if (dtStatusFailed(navMesh_->addTile(navData, navDataSize, DT_TILE_FREE_DATA, 0, 0)))
+    {
+        URHO3D_LOGERROR("Failed to add navigation mesh tile");
+        dtFree(navData);
+        return false;
+    }
+
+    // Send event
+    if (!silent)
+    {
+        using namespace NavigationTileAdded;
+        VariantMap& eventData = GetContext()->GetEventDataMap();
+        eventData[P_NODE] = GetNode();
+        eventData[P_MESH] = this;
+        eventData[P_TILE] = IntVector2(x, z);
+        SendEvent(E_NAVIGATION_TILE_ADDED, eventData);
+    }
+    return true;
+}
+
 bool NavigationMesh::BuildTile(Vector<NavigationGeometryInfo>& geometryList, int x, int z)
 {
     URHO3D_PROFILE(BuildNavigationMeshTile);
@@ -1148,18 +1306,7 @@ bool NavigationMesh::BuildTile(Vector<NavigationGeometryInfo>& geometryList, int
     // Remove previous tile (if any)
     navMesh_->removeTile(navMesh_->getTileRefAt(x, z, 0), 0, 0);
 
-    float tileEdgeLength = (float)tileSize_ * cellSize_;
-
-    BoundingBox tileBoundingBox(Vector3(
-            boundingBox_.min_.x_ + tileEdgeLength * (float)x,
-            boundingBox_.min_.y_,
-            boundingBox_.min_.z_ + tileEdgeLength * (float)z
-        ),
-        Vector3(
-            boundingBox_.min_.x_ + tileEdgeLength * (float)(x + 1),
-            boundingBox_.max_.y_,
-            boundingBox_.min_.z_ + tileEdgeLength * (float)(z + 1)
-        ));
+    const BoundingBox tileBoundingBox = GetTileBoudningBox(IntVector2(x, z));
 
     SimpleNavBuildData build;
 
@@ -1380,6 +1527,21 @@ bool NavigationMesh::BuildTile(Vector<NavigationGeometryInfo>& geometryList, int
     return true;
 }
 
+unsigned NavigationMesh::BuildTiles(Vector<NavigationGeometryInfo>& geometryList, const IntVector2& from, const IntVector2& to)
+{
+    unsigned numTiles = 0;
+
+    for (int z = from.y_; z <= to.y_; ++z)
+    {
+        for (int x = from.x_; x <= to.x_; ++x)
+        {
+            if (BuildTile(geometryList, x, z))
+                ++numTiles;
+        }
+    }
+    return numTiles;
+}
+
 bool NavigationMesh::InitializeQuery()
 {
     if (!navMesh_ || !node_)

+ 27 - 2
Source/Urho3D/Navigation/NavigationMesh.h

@@ -134,10 +134,28 @@ public:
     void SetPadding(const Vector3& padding);
     /// Set the cost of an area.
     void SetAreaCost(unsigned areaID, float cost);
+    /// Allocate the navigation mesh without building any tiles. Bounding box is not padded. Return true if successful.
+    virtual bool Allocate(const BoundingBox& boundingBox, unsigned maxTiles);
     /// Rebuild the navigation mesh. Return true if successful.
     virtual bool Build();
     /// Rebuild part of the navigation mesh contained by the world-space bounding box. Return true if successful.
     virtual bool Build(const BoundingBox& boundingBox);
+    /// Rebuild part of the navigation mesh in the rectangular area. Return true if successful.
+    virtual bool Build(const IntVector2& from, const IntVector2& to);
+    /// Return tile data.
+    virtual PODVector<unsigned char> GetTileData(const IntVector2& tile) const;
+    /// Add tile to navigation mesh.
+    virtual bool AddTile(const PODVector<unsigned char>& tileData);
+    /// Remove tile from navigation mesh.
+    virtual void RemoveTile(const IntVector2& tile);
+    /// Remove all tiles from navigation mesh.
+    virtual void RemoveAllTiles();
+    /// Return whether the navigation mesh has tile.
+    bool HasTile(const IntVector2& tile) const;
+    /// Return bounding box of the tile in the node space.
+    BoundingBox GetTileBoudningBox(const IntVector2& tile) const;
+    /// Return index of the tile at the position.
+    IntVector2 GetTileIndex(const Vector3& position) const;
     /// Find the nearest point on the navigation mesh to a given point. Extents specifies how far out from the specified point to check along each axis.
     Vector3 FindNearestPoint
         (const Vector3& point, const Vector3& extents = Vector3::ONE, const dtQueryFilter* filter = 0, dtPolyRef* nearestRef = 0);
@@ -254,18 +272,25 @@ public:
     /// Return whether to draw NavArea components.
     bool GetDrawNavAreas() const { return drawNavAreas_; }
 
+private:
+    /// Write tile data.
+    void WriteTile(Serializer& dest, int x, int z) const;
+    /// Read tile data to the navigation mesh.
+    bool ReadTile(Deserializer& source, bool silent);
+
 protected:
     /// Collect geometry from under Navigable components.
     void CollectGeometries(Vector<NavigationGeometryInfo>& geometryList);
     /// Visit nodes and collect navigable geometry.
-    void
-        CollectGeometries(Vector<NavigationGeometryInfo>& geometryList, Node* node, HashSet<Node*>& processedNodes, bool recursive);
+    void CollectGeometries(Vector<NavigationGeometryInfo>& geometryList, Node* node, HashSet<Node*>& processedNodes, bool recursive);
     /// Get geometry data within a bounding box.
     void GetTileGeometry(NavBuildData* build, Vector<NavigationGeometryInfo>& geometryList, BoundingBox& box);
     /// Add a triangle mesh to the geometry data.
     void AddTriMeshGeometry(NavBuildData* build, Geometry* geometry, const Matrix3x4& transform);
     /// Build one tile of the navigation mesh. Return true if successful.
     virtual bool BuildTile(Vector<NavigationGeometryInfo>& geometryList, int x, int z);
+    /// Build tiles in the rectangular area. Return number of built tiles.
+    unsigned BuildTiles(Vector<NavigationGeometryInfo>& geometryList, const IntVector2& from, const IntVector2& to);
     /// Ensure that the navigation mesh query is initialized. Return true if successful.
     bool InitializeQuery();
     /// Release the navigation mesh and the query.

+ 13 - 1
Source/Urho3D/Navigation/Obstacle.cpp

@@ -104,13 +104,17 @@ void Obstacle::OnSceneSet(Scene* scene)
         if (!ownerMesh_)
             ownerMesh_ = node_->GetParentComponent<DynamicNavigationMesh>(true);
         if (ownerMesh_)
+        {
             ownerMesh_->AddObstacle(this);
+            SubscribeToEvent(ownerMesh_, E_NAVIGATION_TILE_ADDED, URHO3D_HANDLER(Obstacle, HandleNavigationTileAdded));
+        }
     }
     else
     {
         if (obstacleId_ > 0 && ownerMesh_)
             ownerMesh_->RemoveObstacle(this);
-        
+
+        UnsubscribeFromEvent(E_NAVIGATION_TILE_ADDED);
         ownerMesh_.Reset();
     }
 }
@@ -135,6 +139,14 @@ void Obstacle::OnMarkedDirty(Node* node)
     }
 }
 
+void Obstacle::HandleNavigationTileAdded(StringHash eventType, VariantMap& eventData)
+{
+    // Re-add obstacle if it is intersected with newly added tile
+    const IntVector2 tile = eventData[NavigationTileAdded::P_TILE].GetIntVector2();
+    if (IsEnabledEffective() && ownerMesh_ && ownerMesh_->IsObstacleInTile(this, tile))
+        ownerMesh_->ObstacleChanged(this);
+}
+
 void Obstacle::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
 {
     if (debug && IsEnabledEffective())

+ 2 - 0
Source/Urho3D/Navigation/Obstacle.h

@@ -76,6 +76,8 @@ protected:
     virtual void OnSceneSet(Scene* scene);
     /// Handle node transform being dirtied.
     virtual void OnMarkedDirty(Node* node);
+    /// Handle navigation mesh tile added.
+    void HandleNavigationTileAdded(StringHash eventType, VariantMap& eventData);
 
 private:
     /// Radius of this obstacle.

+ 73 - 1
bin/Data/LuaScripts/15_Navigation.lua

@@ -12,6 +12,11 @@ require "LuaScripts/Utilities/Sample"
 local endPos = nil
 local currentPath = {}
 
+local useStreaming = false
+local streamingDistance = 2
+local navigationTiles = {}
+local addedTiles = {}
+
 function Start()
     -- Execute the common startup for samples
     SampleStart()
@@ -98,6 +103,8 @@ function CreateScene()
 
     -- Create a NavigationMesh component to the scene root
     local navMesh = scene_:CreateComponent("NavigationMesh")
+    -- Set small tiles to show navigation mesh streaming
+    navMesh.tileSize = 32
     -- Create a Navigable component to the scene root. This tags all of the geometry in the scene as being part of the
     -- navigation mesh. By default this is recursive, but the recursion could be turned off from Navigable
     scene_:CreateComponent("Navigable")
@@ -135,6 +142,7 @@ function CreateUI()
     instructionText.text = "Use WASD keys to move, RMB to rotate view\n"..
         "LMB to set destination, SHIFT+LMB to teleport\n"..
         "MMB or O key to add or remove obstacles\n"..
+        "Tab to toggle navigation mesh streaming\n"..
         "Space to toggle debug geometry"
     instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
     -- The text has multiple rows. Center them in relation to each other
@@ -242,7 +250,7 @@ end
 function AddOrRemoveObject()
     -- Raycast and check if we hit a mushroom node. If yes, remove it, if no, create a new one
     local hitPos, hitDrawable = Raycast(250.0)
-    if hitDrawable then
+    if not useStreaming and hitDrawable then
         -- The part of the navigation mesh we must update, which is the world bounding box of the associated
         -- drawable component
         local updateBox = nil
@@ -298,6 +306,61 @@ function Raycast(maxDistance)
     return nil, nil
 end
 
+function ToggleStreaming(enabled)
+    local navMesh = scene_:GetComponent("NavigationMesh")
+    if enabled then
+        local maxTiles = (2 * streamingDistance + 1) * (2 * streamingDistance + 1)
+        local boundingBox = BoundingBox(navMesh.boundingBox)
+        SaveNavigationData()
+        navMesh:Allocate(boundingBox, maxTiles);
+    else
+        navMesh:Build();
+    end
+end
+
+function UpdateStreaming()
+    local navMesh = scene_:GetComponent("NavigationMesh")
+
+    -- Center the navigation mesh at the jack
+    local jackTile = navMesh:GetTileIndex(jackNode.worldPosition)
+    local beginTile = VectorMax(IntVector2(0, 0), jackTile - IntVector2(1, 1) * streamingDistance)
+    local endTile = VectorMin(jackTile + IntVector2(1, 1) * streamingDistance, navMesh.numTiles - IntVector2(1, 1))
+
+    -- Remove tiles
+    local numTiles = navMesh.numTiles
+    for i,tileIdx in pairs(addedTiles) do
+        if not (beginTile.x <= tileIdx.x and tileIdx.x <= endTile.x and beginTile.y <= tileIdx.y and tileIdx.y <= endTile.y) then
+            addedTiles[i] = nil
+            navMesh:RemoveTile(tileIdx)
+        end
+    end
+
+    -- Add tiles
+    for z = beginTile.y, endTile.y do
+        for x = beginTile.x, endTile.x do
+            local i = z * numTiles.x + x
+            if not navMesh:HasTile(IntVector2(x, z)) and navigationTiles[i] then
+                addedTiles[i] = IntVector2(x, z)
+                navMesh:AddTile(navigationTiles[i])
+            end
+        end
+    end
+end
+
+function SaveNavigationData()
+    local navMesh = scene_:GetComponent("NavigationMesh")
+    navigationTiles = {}
+    addedTiles = {}
+    local numTiles = navMesh.numTiles
+
+    for z = 0, numTiles.y - 1 do
+        for x = 0, numTiles.x - 1 do
+            local i = z * numTiles.x + x
+            navigationTiles[i] = navMesh:GetTileData(IntVector2(x, z))
+        end
+    end
+end
+
 function HandleUpdate(eventType, eventData)
     -- Take the frame time step, which is stored as a float
     local timeStep = eventData["TimeStep"]:GetFloat()
@@ -307,6 +370,15 @@ function HandleUpdate(eventType, eventData)
 
     -- Make Jack follow the Detour path
     FollowPath(timeStep)
+
+    -- Update streaming
+    if input:GetKeyPress(KEY_TAB) then
+        useStreaming = not useStreaming
+        ToggleStreaming(useStreaming)
+    end
+    if useStreaming then
+        UpdateStreaming()
+    end
 end
 
 function FollowPath(timeStep)

+ 80 - 0
bin/Data/LuaScripts/39_CrowdNavigation.lua

@@ -13,6 +13,11 @@ require "LuaScripts/Utilities/Sample"
 
 local INSTRUCTION = "instructionText"
 
+local useStreaming = false
+local streamingDistance = 2
+local navigationTiles = {}
+local addedTiles = {}
+
 function Start()
     -- Execute the common startup for samples
     SampleStart()
@@ -85,6 +90,8 @@ function CreateScene()
 
     -- Create a DynamicNavigationMesh component to the scene root
     local navMesh = scene_:CreateComponent("DynamicNavigationMesh")
+    -- Set small tiles to show navigation mesh streaming
+    navMesh.tileSize = 32
     -- Enable drawing debug geometry for obstacles and off-mesh connections
     navMesh.drawObstacles = true
     navMesh.drawOffMeshConnections = true
@@ -157,6 +164,7 @@ function CreateUI()
         "LMB to set destination, SHIFT+LMB to spawn a Jack\n"..
         "MMB or O key to add obstacles or remove obstacles/agents\n"..
         "F5 to save scene, F7 to load\n"..
+        "Tab to toggle navigation mesh streaming\n"..
         "Space to toggle debug geometry\n"..
         "F12 to toggle this instruction text"
     instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
@@ -385,6 +393,69 @@ function MoveCamera(timeStep)
         instruction.visible = not instruction.visible
     end
 end
+function ToggleStreaming(enabled)
+    local navMesh = scene_:GetComponent("DynamicNavigationMesh")
+    if enabled then
+        local maxTiles = (2 * streamingDistance + 1) * (2 * streamingDistance + 1)
+        local boundingBox = BoundingBox(navMesh.boundingBox)
+        SaveNavigationData()
+        navMesh:Allocate(boundingBox, maxTiles);
+    else
+        navMesh:Build();
+    end
+end
+
+function UpdateStreaming()
+    local navMesh = scene_:GetComponent("DynamicNavigationMesh")
+
+    -- Center the navigation mesh at the jacks crowd
+    local averageJackPosition = Vector3(0, 0, 0)
+    local jackGroup = scene_:GetChild("Jacks")
+    if jackGroup then
+        for i = 0,jackGroup:GetNumChildren()-1 do
+            averageJackPosition = averageJackPosition + jackGroup:GetChild(i).worldPosition
+        end
+        averageJackPosition = averageJackPosition / jackGroup:GetNumChildren()
+    end
+
+    local jackTile = navMesh:GetTileIndex(averageJackPosition)
+    local beginTile = VectorMax(IntVector2(0, 0), jackTile - IntVector2(1, 1) * streamingDistance)
+    local endTile = VectorMin(jackTile + IntVector2(1, 1) * streamingDistance, navMesh.numTiles - IntVector2(1, 1))
+
+    -- Remove tiles
+    local numTiles = navMesh.numTiles
+    for i,tileIdx in pairs(addedTiles) do
+        if not (beginTile.x <= tileIdx.x and tileIdx.x <= endTile.x and beginTile.y <= tileIdx.y and tileIdx.y <= endTile.y) then
+            addedTiles[i] = nil
+            navMesh:RemoveTile(tileIdx)
+        end
+    end
+
+    -- Add tiles
+    for z = beginTile.y, endTile.y do
+        for x = beginTile.x, endTile.x do
+            local i = z * numTiles.x + x
+            if not navMesh:HasTile(IntVector2(x, z)) and navigationTiles[i] then
+                addedTiles[i] = IntVector2(x, z)
+                navMesh:AddTile(navigationTiles[i])
+            end
+        end
+    end
+end
+
+function SaveNavigationData()
+    local navMesh = scene_:GetComponent("DynamicNavigationMesh")
+    navigationTiles = {}
+    addedTiles = {}
+    local numTiles = navMesh.numTiles
+
+    for z = 0, numTiles.y - 1 do
+        for x = 0, numTiles.x - 1 do
+            local i = z * numTiles.x + x
+            navigationTiles[i] = navMesh:GetTileData(IntVector2(x, z))
+        end
+    end
+end
 
 function HandleUpdate(eventType, eventData)
     -- Take the frame time step, which is stored as a float
@@ -392,6 +463,15 @@ function HandleUpdate(eventType, eventData)
 
     -- Move the camera, scale movement with time step
     MoveCamera(timeStep)
+
+    -- Update streaming
+    if input:GetKeyPress(KEY_TAB) then
+        useStreaming = not useStreaming
+        ToggleStreaming(useStreaming)
+    end
+    if useStreaming then
+        UpdateStreaming()
+    end
 end
 
 function HandlePostRenderUpdate(eventType, eventData)

+ 85 - 1
bin/Data/Scripts/15_Navigation.as

@@ -12,6 +12,12 @@
 Vector3 endPos;
 Array<Vector3> currentPath;
 Node@ jackNode;
+bool useStreaming = false;
+// Used for streaming only
+const int STREAMING_DISTANCE = 2;
+Array<VectorBuffer> navigationTilesData;
+Array<IntVector2> navigationTilesIdx;
+Array<IntVector2> addedTiles;
 
 void Start()
 {
@@ -101,6 +107,8 @@ void CreateScene()
 
     // Create a NavigationMesh component to the scene root
     NavigationMesh@ navMesh = scene_.CreateComponent("NavigationMesh");
+    // Set small tiles to show navigation mesh streaming
+    navMesh.tileSize = 32;
     // Create a Navigable component to the scene root. This tags all of the geometry in the scene as being part of the
     // navigation mesh. By default this is recursive, but the recursion could be turned off from Navigable
     scene_.CreateComponent("Navigable");
@@ -140,6 +148,7 @@ void CreateUI()
         "Use WASD keys to move, RMB to rotate view\n"
         "LMB to set destination, SHIFT+LMB to teleport\n"
         "MMB or O key to add or remove obstacles\n"
+        "Tab to toggle navigation mesh streaming\n"
         "Space to toggle debug geometry";
     instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
     // The text has multiple rows. Center them in relation to each other
@@ -255,7 +264,7 @@ void AddOrRemoveObject()
     Vector3 hitPos;
     Drawable@ hitDrawable;
 
-    if (Raycast(250.0f, hitPos, hitDrawable))
+    if (!useStreaming && Raycast(250.0f, hitPos, hitDrawable))
     {
         // The part of the navigation mesh we must update, which is the world bounding box of the associated
         // drawable component
@@ -341,6 +350,72 @@ void FollowPath(float timeStep)
     }
 }
 
+void ToggleStreaming(bool enabled)
+{
+    NavigationMesh@ navMesh = scene_.GetComponent("NavigationMesh");
+    if (enabled)
+    {
+        int maxTiles = (2 * STREAMING_DISTANCE + 1) * (2 * STREAMING_DISTANCE + 1);
+        BoundingBox boundingBox = navMesh.boundingBox;
+        SaveNavigationData();
+        navMesh.Allocate(boundingBox, maxTiles);
+    }
+    else
+        navMesh.Build();
+}
+
+void UpdateStreaming()
+{
+    NavigationMesh@ navMesh = scene_.GetComponent("NavigationMesh");
+
+    // Center the navigation mesh at the jack
+    IntVector2 jackTile = navMesh.GetTileIndex(jackNode.worldPosition);
+    IntVector2 beginTile = VectorMax(IntVector2(0, 0), jackTile - IntVector2(1, 1) * STREAMING_DISTANCE);
+    IntVector2 endTile = VectorMin(jackTile + IntVector2(1, 1) * STREAMING_DISTANCE, navMesh.numTiles - IntVector2(1, 1));
+
+    // Remove tiles
+    for (uint i = 0; i < addedTiles.length;)
+    {
+        IntVector2 tileIdx = addedTiles[i];
+        if (beginTile.x <= tileIdx.x && tileIdx.x <= endTile.x && beginTile.y <= tileIdx.y && tileIdx.y <= endTile.y)
+            ++i;
+        else
+        {
+            addedTiles.Erase(i);
+            navMesh.RemoveTile(tileIdx);
+        }
+    }
+
+    // Add tiles
+    for (int z = beginTile.y; z <= endTile.y; ++z)
+        for (int x = beginTile.x; x <= endTile.x; ++x)
+        {
+            const IntVector2 tileIdx(x, z);
+            int tileDataIdx = navigationTilesIdx.Find(tileIdx);
+            if (!navMesh.HasTile(tileIdx) && tileDataIdx != -1)
+            {
+                addedTiles.Push(tileIdx);
+                navMesh.AddTile(navigationTilesData[tileDataIdx]);
+            }
+        }
+}
+
+void SaveNavigationData()
+{
+    NavigationMesh@ navMesh = scene_.GetComponent("NavigationMesh");
+    navigationTilesData.Clear();
+    navigationTilesIdx.Clear();
+    addedTiles.Clear();
+    IntVector2 numTiles = navMesh.numTiles;
+    for (int z = 0; z < numTiles.y; ++z)
+        for (int x = 0; x < numTiles.x; ++x)
+        {
+            IntVector2 idx(x, z);
+            navigationTilesData.Push(navMesh.GetTileData(idx));
+            navigationTilesIdx.Push(idx);
+        }
+}
+
 void HandleUpdate(StringHash eventType, VariantMap& eventData)
 {
     // Take the frame time step, which is stored as a float
@@ -351,6 +426,15 @@ void HandleUpdate(StringHash eventType, VariantMap& eventData)
 
     // Make Jack follow the Detour path
     FollowPath(timeStep);
+
+    // Update streaming
+    if (input.keyPress[KEY_TAB])
+    {
+        useStreaming = !useStreaming;
+        ToggleStreaming(useStreaming);
+    }
+    if (useStreaming)
+        UpdateStreaming();
 }
 
 void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)

+ 95 - 0
bin/Data/Scripts/39_CrowdNavigation.as

@@ -13,6 +13,13 @@
 
 const String INSTRUCTION("instructionText");
 
+bool useStreaming = false;
+// Used for streaming only
+const int STREAMING_DISTANCE = 2;
+Array<VectorBuffer> navigationTilesData;
+Array<IntVector2> navigationTilesIdx;
+Array<IntVector2> addedTiles;
+
 void Start()
 {
     // Execute the common startup for samples
@@ -88,6 +95,8 @@ void CreateScene()
 
     // Create a DynamicNavigationMesh component to the scene root
     DynamicNavigationMesh@ navMesh = scene_.CreateComponent("DynamicNavigationMesh");
+    // Set small tiles to show navigation mesh streaming
+    navMesh.tileSize = 32;
     // Enable drawing debug geometry for obstacles and off-mesh connections
     navMesh.drawObstacles = true;
     navMesh.drawOffMeshConnections = true;
@@ -161,6 +170,7 @@ void CreateUI()
         "LMB to set destination, SHIFT+LMB to spawn a Jack\n"
         "MMB or O key to add obstacles or remove obstacles/agents\n"
         "F5 to save scene, F7 to load\n"
+        "Tab to toggle navigation mesh streaming\n"
         "Space to toggle debug geometry\n"
         "F12 to toggle this instruction text";
     instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
@@ -414,6 +424,82 @@ void MoveCamera(float timeStep)
     }
 }
 
+void ToggleStreaming(bool enabled)
+{
+    DynamicNavigationMesh@ navMesh = scene_.GetComponent("DynamicNavigationMesh");
+    if (enabled)
+    {
+        int maxTiles = (2 * STREAMING_DISTANCE + 1) * (2 * STREAMING_DISTANCE + 1);
+        BoundingBox boundingBox = navMesh.boundingBox;
+        SaveNavigationData();
+        navMesh.Allocate(boundingBox, maxTiles);
+    }
+    else
+        navMesh.Build();
+}
+
+void UpdateStreaming()
+{
+    DynamicNavigationMesh@ navMesh = scene_.GetComponent("DynamicNavigationMesh");
+
+    // Center the navigation mesh at the crowd of jacks
+    Vector3 averageJackPosition;
+    Node@ jackGroup = scene_.GetChild("Jacks");
+    if (jackGroup !is null)
+    {
+        for (uint i = 0; i < jackGroup.numChildren; ++i)
+            averageJackPosition += jackGroup.children[i].worldPosition;
+        averageJackPosition /= jackGroup.numChildren;
+    }
+
+    // Compute currently loaded area
+    IntVector2 jackTile = navMesh.GetTileIndex(averageJackPosition);
+    IntVector2 beginTile = VectorMax(IntVector2(0, 0), jackTile - IntVector2(1, 1) * STREAMING_DISTANCE);
+    IntVector2 endTile = VectorMin(jackTile + IntVector2(1, 1) * STREAMING_DISTANCE, navMesh.numTiles - IntVector2(1, 1));
+
+    // Remove tiles
+    for (uint i = 0; i < addedTiles.length;)
+    {
+        IntVector2 tileIdx = addedTiles[i];
+        if (beginTile.x <= tileIdx.x && tileIdx.x <= endTile.x && beginTile.y <= tileIdx.y && tileIdx.y <= endTile.y)
+            ++i;
+        else
+        {
+            addedTiles.Erase(i);
+            navMesh.RemoveTile(tileIdx);
+        }
+    }
+
+    // Add tiles
+    for (int z = beginTile.y; z <= endTile.y; ++z)
+        for (int x = beginTile.x; x <= endTile.x; ++x)
+        {
+            const IntVector2 tileIdx(x, z);
+            int tileDataIdx = navigationTilesIdx.Find(tileIdx);
+            if (!navMesh.HasTile(tileIdx) && tileDataIdx != -1)
+            {
+                addedTiles.Push(tileIdx);
+                navMesh.AddTile(navigationTilesData[tileDataIdx]);
+            }
+        }
+}
+
+void SaveNavigationData()
+{
+    DynamicNavigationMesh@ navMesh = scene_.GetComponent("DynamicNavigationMesh");
+    navigationTilesData.Clear();
+    navigationTilesIdx.Clear();
+    addedTiles.Clear();
+    IntVector2 numTiles = navMesh.numTiles;
+    for (int z = 0; z < numTiles.y; ++z)
+        for (int x = 0; x < numTiles.x; ++x)
+        {
+            IntVector2 idx(x, z);
+            navigationTilesData.Push(navMesh.GetTileData(idx));
+            navigationTilesIdx.Push(idx);
+        }
+}
+
 void HandleUpdate(StringHash eventType, VariantMap& eventData)
 {
     // Take the frame time step, which is stored as a float
@@ -421,6 +507,15 @@ void HandleUpdate(StringHash eventType, VariantMap& eventData)
 
     // Move the camera, scale movement with time step
     MoveCamera(timeStep);
+
+    // Update streaming
+    if (input.keyPress[KEY_TAB])
+    {
+        useStreaming = !useStreaming;
+        ToggleStreaming(useStreaming);
+    }
+    if (useStreaming)
+        UpdateStreaming();
 }
 
 void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)