This project started out with the goal of gaining an understanding of how navigation meshes are generated. Soon this evolved into developing a tool that I can utilise to explore different pathing algorithms and AI behaviours. To do so, I needed to incorporate a way to quickly create levels that I can use to add path agents into so I can test out the desired algorithms and behaviours. These levels would contain static objects that would act as obstacles which the path agents can navigate around.
The mesh is built using a variation of the Bowyer-Watson algorithm to generate the Delaunay Triangulation of the points. For a Delaunay Triangulation to occur, all points in the mesh mustn’t fall into the circumcircle of another triangle, thus defining this as the Delaunay rule. The process follows these steps:
Create a super triangle to encompass the area.
Add a new point into the triangulation.
Remove the triangle that contains the newly added point.
Create 3 new triangles by connecting the newly added point to the three vertices of the former triangle.
Check if the point falls into the circumcircle of any other triangle
If it does, flip the edge that is adjacent to the point, changing the two triangles that share it.
Repeat until all points have been added.
Remove the super triangle.
Fortunately, a formula exists for calculating The Cartesian coordinates of the circumcenter(1). Once the center is found, I use Heron’s formula to find the area of the circle by substituting in values of the lengths of each side of the triangle and the triangles half perimeter. Finally, it’s a simple circle vs circle collision check to see if the distance of the point to the circumcenter is less than the radius of the circumcircle.
Circumcircle of the triangle created by the three red vertices
This next step proved to be the most difficult step for me during this project, which I talk about in more detail below. There are many ways you could handle this step all with their own challenges, but I decided to follow the more straightforward approach of completing a series of line intersection tests. I achieved this by breaking down each obstacle into edges and each triangle into edges, then checking the two sets of edges against each other to see if an overlap occurs. This check is completed by doing a series of cross product checks. For triangle edge T1 to T2 and for obstacle edge O1 to O2, I check:
A. (O2-O1) Cross (T1- O1)
B. (O2-O1) Cross (T2 - O1)
C. (T2-T1) Cross (O1 - T2)
D. (T2-T1) Cross (O2 - T2)
If there is an overlap, we add the triangle points to a list. Once I have checked all triangles, I then determine which side of the constraint edge each point falls on and create two corresponding lists that contain the respective points. Then all these points are triangulated using the already created Delaunay triangulation function and add them to our triangle stack.
Since the obstacles are areas which the agent can't navigate, I remove any triangles that are inside of them. Once this is done, I complete one more iteration over the triangles to restore the Delaunayness of them. This is to ensure the mesh is fully successful by checking that all the triangles adhered to the Delaunay rule. During this step I was introduced to the ‘case of the perfect square’, where it is impossible to divide a square into two triangles that adhere to the Delaunay rule. The program was getting stuck in an infinite loop, where the two same triangles were constantly being flipped. To resolve this, I added a safety check to identify when this would occur and break out of the loop if needed.
Perfect square created by
two triangles
My next hurdle was to figure out how this mesh can be used for navigation by our agents. In previous projects, I used node graphs as a way to determine the navigable areas for the agent and realised that this was something I can also use here. I constructed the node graph using the generated triangles, where the center point of each triangle will become a node in the graph. From here, I simply connected each node to the adjacent triangles of each edge, resulting in a graph structure that the agent can then move around using the node connections.
Node graph created from the nav mesh
Adding in constraints was the most challenging step of this whole process, where I stumbled across a number of different edge cases that were difficult to identify. My original solution to resolving constraint overlaps was to find the two triangles that share the overlapping edge, and flip is to create two new triangles. I had issues in successfully identifying adjacent edges to flip, and sometimes when I did find an edge, flipping it didn't resolve the overlap because it would cause a butterfly effect that rippled back to its original state. I tried a number of solutions, including restarting the project and introducing a data structure to help keep track of edges (before finding out about half edge meshes). Until finally coming to the solution described above.
Another notable challenge was correctly identifying which lines were truly overlapping a constraint edge. Originally when completing the cross product checks (mentioned above) I wasn't including any results equal to 0, meaning any lines passing through the vertex of a constraint edge weren't being detected as an overlap.
Line overlaps on the edge of the constraint
I found out quickly that changing the conditional check from “A x B < 0” to “A x B <= 0” wasn’t enough to solve this issue, as some lines connected to the edge of a constraint would classify as an overlap despite that not being the case. So I added in a simple check to see if the line was connected to either end of the constraint, if so then move on to the next line.
Resulting nav mesh before detecting if the line was connected to
the edge of a constraint
Resulting nav mesh after implemeting the line-edge detection
But after some stress testing, I was still having incorrect overlaps being detected. I found that line segments that fall on the same axis of the constraint edge were also being detected as an overlap. To address this, one final check was completed to see if the two points of the line fell on opposite sides of the constraint line. This was done in the form of two cross products; One from the first point of the constraint edge to each point of the overlapping line, and one from the second point of the constraint edge to each point of the overlapping line. If both return 0, then the line is collinear with the constraint edge and should not be considered an overlap.
Cross product check to see if the points fall on opposite sides of the constraint
There are a number of different things I want to explore to further improve this project. Currently the level generation is limited where areas within hollowed obstacles are not navigable. Adjusting the level generation process to include areas within obstacles will provide more flexibility to explore different AI behaviours. Unity’s inbuilt nav mesh system allows pathing agents to jump across from one mesh to another, which is another feature I’d like to introduce into this project. Furthermore, there are lots of other adjustable parameters such as step height, area costs and exclusion zones which I'd like to explore further.
Moving obstacles is another feature I’d like to implement. When researching navigation meshes before starting this process, I read an article(2) which showcased how the functionality of updating a mesh to allow for moving obstacles would work, and this was something that had stuck with me throughout this project. Isolating and retriangulating the area which the obstacle affects seems straightforward at first, until you experience the number of edge cases it introduces. For my first attempt at this project, I did begin the process of having a dynamically updating mesh, but quickly scrapped that until I was able to solidify the base constraint solving first with hopes to return to the idea of moving obstacles.
This data structure could have solved a number of issues I had throughout this project, but was only something I was introduced to at the later stages of the project. This would help to easily navigate the triangles when searching for neighbours and adjacent triangles, which notably was something I found challenging. Introducing this data structure can assist with optimisation, though usually meshes are baked once. More so when looking to implement moving obstacles, this will significantly improve performance when having to retriangulate the areas during runtime.
https://en.wikipedia.org/wiki/Circumcircle#Circumcenter_coordinates
https://www.jdxdev.com/blog/2021/07/06/rts-pathfinding-2-dynamic-navmesh-with-constrained-delaunay-triangles/
S.W.Sloan (1992). A Fast Algorithm For Generating Constrained Delaunay Triangulation
S.W.Sloan (1987). A fast algorithm for constructing Delaunay triangulations in the plane