2025-07-08 10:46:31 +00:00
namespace Pathfinding.RVO {
using Pathfinding ;
using UnityEngine ;
using Pathfinding.Util ;
using Unity.Mathematics ;
using Unity.Collections ;
using Pathfinding.Collections ;
using System.Collections.Generic ;
using Unity.Burst ;
using Unity.Profiling ;
using Pathfinding.Sync ;
#if MODULE_COLLECTIONS_2_1_0_OR_NEWER
using NativeHashMapIntInt = Unity . Collections . NativeHashMap < int , int > ;
#else
using NativeHashMapIntInt = Unity . Collections . NativeParallelHashMap < int , int > ;
#endif
[BurstCompile]
public static class RVOObstacleCache {
public struct ObstacleSegment {
public float3 vertex1 ;
public float3 vertex2 ;
public int vertex1LinkId ;
public int vertex2LinkId ;
}
static ulong HashKey ( GraphNode sourceNode , int traversableTags , SimpleMovementPlane movementPlane ) {
var hash = ( ulong ) sourceNode . NodeIndex ;
hash = hash * 786433 ^ ( ulong ) traversableTags ;
// The rotation is not particularly important for the obstacle. It's only used
// to simplify the output a bit. So we allow similar rotations to share the same hash.
const float RotationQuantization = 4 ;
hash = hash * 786433 ^ ( ulong ) ( movementPlane . rotation . x * RotationQuantization ) ;
hash = hash * 786433 ^ ( ulong ) ( movementPlane . rotation . y * RotationQuantization ) ;
hash = hash * 786433 ^ ( ulong ) ( movementPlane . rotation . z * RotationQuantization ) ;
hash = hash * 786433 ^ ( ulong ) ( movementPlane . rotation . w * RotationQuantization ) ;
return hash ;
}
/// <summary>
/// Collects an unordered list of contour segments based on the given nodes.
///
/// Note: All nodes must be from the same graph.
/// </summary>
public static void CollectContours ( List < GraphNode > nodes , NativeList < ObstacleSegment > obstacles ) {
if ( nodes . Count = = 0 ) return ;
if ( nodes [ 0 ] is TriangleMeshNode ) {
for ( int i = 0 ; i < nodes . Count ; i + + ) {
var tnode = nodes [ i ] as TriangleMeshNode ;
var used = 0 ;
if ( tnode . connections ! = null ) {
for ( int j = 0 ; j < tnode . connections . Length ; j + + ) {
var conn = tnode . connections [ j ] ;
if ( conn . isEdgeShared ) {
used | = 1 < < conn . shapeEdge ;
}
}
}
tnode . GetVertices ( out var v0 , out var v1 , out var v2 ) ;
for ( int edgeIndex = 0 ; edgeIndex < 3 ; edgeIndex + + ) {
if ( ( used & ( 1 < < edgeIndex ) ) = = 0 ) {
// This edge is not shared, therefore it's a contour edge
Int3 leftVertex , rightVertex ;
switch ( edgeIndex ) {
case 0 :
leftVertex = v0 ;
rightVertex = v1 ;
break ;
case 1 :
leftVertex = v1 ;
rightVertex = v2 ;
break ;
case 2 :
default :
leftVertex = v2 ;
rightVertex = v0 ;
break ;
}
var leftVertexHash = leftVertex . GetHashCode ( ) ;
var rightVertexHash = rightVertex . GetHashCode ( ) ;
obstacles . Add ( new ObstacleSegment {
vertex1 = ( Vector3 ) leftVertex ,
vertex2 = ( Vector3 ) rightVertex ,
vertex1LinkId = leftVertexHash ,
vertex2LinkId = rightVertexHash ,
} ) ;
}
}
}
} else if ( nodes [ 0 ] is GridNodeBase ) {
GridGraph graph ;
if ( nodes [ 0 ] is LevelGridNode ) graph = LevelGridNode . GetGridGraph ( nodes [ 0 ] . GraphIndex ) ;
else
graph = GridNode . GetGridGraph ( nodes [ 0 ] . GraphIndex ) ;
unsafe {
// Offsets from the center of the node to the corners of the node, in world space
// Index dir is the offset to the left corner of the edge in direction dir
// See GridNodeBase.GetNeighbourAlongDirection for the direction indices
Vector3 * offsets = stackalloc Vector3 [ 4 ] ;
for ( int dir = 0 ; dir < 4 ; dir + + ) {
var dl = ( dir + 1 ) % 4 ;
offsets [ dir ] = graph . transform . TransformVector ( 0.5f * new Vector3 ( GridGraph . neighbourXOffsets [ dir ] + GridGraph . neighbourXOffsets [ dl ] , 0 , GridGraph . neighbourZOffsets [ dir ] + GridGraph . neighbourZOffsets [ dl ] ) ) ;
}
for ( int i = 0 ; i < nodes . Count ; i + + ) {
var gnode = nodes [ i ] as GridNodeBase ;
if ( gnode . HasConnectionsToAllAxisAlignedNeighbours ) continue ;
for ( int dir = 0 ; dir < 4 ; dir + + ) {
if ( ! gnode . HasConnectionInDirection ( dir ) ) {
// ┌─────────┬─────────┐
// │ │ │
// │ nl1 │ nl2 │ ^
// │ │ │ |
// ├────────vL─────────┤ dl
// │ │#########│
// │ node │#########│ dir->
// │ │#########│
// ├────────vR─────────┤ dr
// │ │ │ |
// │ nr1 │ nr2 │ v
// │ │ │
// └─────────┴─────────┘
var dl = ( dir + 1 ) % 4 ;
var dr = ( dir - 1 + 4 ) % 4 ;
var nl1 = gnode . GetNeighbourAlongDirection ( dl ) ;
var nr1 = gnode . GetNeighbourAlongDirection ( dr ) ;
// All this hashing code looks slow, but really it's not compared to all the memory accesses to get the node data
uint leftVertexHash ;
if ( nl1 ! = null ) {
var nl2 = nl1 . GetNeighbourAlongDirection ( dir ) ;
if ( nl2 ! = null ) {
// Outer corner. Uniquely determined by the 3 nodes that touch the corner.
var a = gnode . NodeIndex ;
var b = nl1 . NodeIndex ;
var c = nl2 . NodeIndex ;
// Sort the values so that a <= b <= c
if ( a > b ) Memory . Swap ( ref a , ref b ) ;
if ( b > c ) Memory . Swap ( ref b , ref c ) ;
if ( a > b ) Memory . Swap ( ref a , ref b ) ;
leftVertexHash = math . hash ( new uint3 ( a , b , c ) ) ;
} else {
// Straight wall. Uniquely determined by the 2 nodes that touch the corner and the direction to the wall.
var a = gnode . NodeIndex ;
var b = nl1 . NodeIndex ;
// Sort the values so that a <= b
if ( a > b ) Memory . Swap ( ref a , ref b ) ;
leftVertexHash = math . hash ( new uint3 ( a , b , ( uint ) dir ) ) ;
}
} else {
// Inner corner. Uniquely determined by the single node that touches the corner and the direction to it.
var diagonalToCorner = dir + 4 ;
leftVertexHash = math . hash ( new uint2 ( gnode . NodeIndex , ( uint ) diagonalToCorner ) ) ;
}
uint rightVertexHash ;
if ( nr1 ! = null ) {
var nr2 = nr1 . GetNeighbourAlongDirection ( dir ) ;
if ( nr2 ! = null ) {
// Outer corner. Uniquely determined by the 3 nodes that touch the corner.
var a = gnode . NodeIndex ;
var b = nr1 . NodeIndex ;
var c = nr2 . NodeIndex ;
// Sort the values so that a <= b <= c
if ( a > b ) Memory . Swap ( ref a , ref b ) ;
if ( b > c ) Memory . Swap ( ref b , ref c ) ;
if ( a > b ) Memory . Swap ( ref a , ref b ) ;
rightVertexHash = math . hash ( new uint3 ( a , b , c ) ) ;
} else {
// Straight wall. Uniquely determined by the 2 nodes that touch the corner and the direction to the wall.
var a = gnode . NodeIndex ;
var b = nr1 . NodeIndex ;
// Sort the values so that a <= b
if ( a > b ) Memory . Swap ( ref a , ref b ) ;
rightVertexHash = math . hash ( new uint3 ( a , b , ( uint ) dir ) ) ;
}
} else {
// Inner corner. Uniquely determined by the single node that touches the corner and the direction to it.
// Note: It's not a typo that we use `dr+4` here and `dir+4` above. They are different directions.
var diagonalToCorner = dr + 4 ;
rightVertexHash = math . hash ( new uint2 ( gnode . NodeIndex , ( uint ) diagonalToCorner ) ) ;
}
var nodePos = ( Vector3 ) gnode . position ;
obstacles . Add ( new ObstacleSegment {
vertex1 = nodePos + offsets [ dir ] , // Left corner. Yes, it should be dir, not dl, as the offsets array already points to the left corners of each segment.
vertex2 = nodePos + offsets [ dr ] , // Right corner
vertex1LinkId = ( int ) leftVertexHash ,
vertex2LinkId = ( int ) rightVertexHash ,
} ) ;
}
}
}
}
}
}
private static readonly ProfilerMarker MarkerAllocate = new ProfilerMarker ( "Allocate" ) ;
/// <summary>Trace contours generated by CollectContours.</summary>
/// <param name="obstaclesSpan">Obstacle segments, typically the borders of the navmesh. In no particular order.
/// Each edge must be oriented the same way (e.g. all clockwise, or all counter-clockwise around the obstacles).</param>
/// <param name="movementPlane">The movement plane used for simplification. The up direction will be treated as less important for purposes of simplification.</param>
/// <param name="obstacleId">The ID of the obstacle to write into the outputObstacles array.</param>
/// <param name="outputObstacles">Array to write the obstacle to.</param>
/// <param name="verticesAllocator">Allocator to use for the vertices of the obstacle.</param>
/// <param name="obstaclesAllocator">Allocator to use for the obstacle metadata.</param>
/// <param name="spinLock">Lock to use when allocating from the allocators.</param>
/// <param name="simplifyObstacles">If true, the obstacle will be simplified. This means that colinear vertices (when projected onto the movement plane) will be removed.</param>
[BurstCompile]
internal static unsafe void TraceContours ( ref UnsafeSpan < ObstacleSegment > obstaclesSpan , ref NativeMovementPlane movementPlane , int obstacleId , UnmanagedObstacle * outputObstacles , ref SlabAllocator < float3 > verticesAllocator , ref SlabAllocator < ObstacleVertexGroup > obstaclesAllocator , ref SpinLock spinLock , bool simplifyObstacles ) {
var obstacles = obstaclesSpan ;
if ( obstacles . Length = = 0 ) {
outputObstacles [ obstacleId ] = new UnmanagedObstacle {
verticesAllocation = SlabAllocator < float3 > . ZeroLengthArray ,
groupsAllocation = SlabAllocator < ObstacleVertexGroup > . ZeroLengthArray ,
} ;
return ;
}
MarkerAllocate . Begin ( ) ;
var traceLookup = new NativeHashMapIntInt ( obstacles . Length , Unity . Collections . Allocator . Temp ) ;
// For each edge: Will be 0 if the segment should be ignored or if it has been visited, 1 if it has not been visited and has an ingoing edge, and 2 if it has not been visited and has no ingoing edge.
var priority = new NativeArray < byte > ( obstacles . Length , Unity . Collections . Allocator . Temp , Unity . Collections . NativeArrayOptions . UninitializedMemory ) ;
MarkerAllocate . End ( ) ;
for ( int i = 0 ; i < obstacles . Length ; i + + ) {
// var obstacle = obstacles[i];
// Add the edge to the lookup. But if it already exists, ignore it.
// That it already exists is very much a special case that can happen if there is
// overlapping geometry (typically added by a NavmeshAdd component).
// In that case the "outer edge" that we want to trace is kinda undefined, but we will
// do our best with it.
if ( traceLookup . TryAdd ( obstacles [ i ] . vertex1LinkId , i ) ) {
priority [ i ] = 2 ;
} else {
priority [ i ] = 0 ;
}
}
for ( int i = 0 ; i < obstacles . Length ; i + + ) {
if ( traceLookup . TryGetValue ( obstacles [ i ] . vertex2LinkId , out var other ) & & priority [ other ] > 0 ) {
// The other segment has an ingoing edge. This means it cannot be the start of a contour.
// Reduce the priority so that we only consider it when looking for cycles.
priority [ other ] = 1 ;
}
}
var outputMetadata = new NativeList < ObstacleVertexGroup > ( 16 , Allocator . Temp ) ;
var outputVertices = new NativeList < float3 > ( 16 , Allocator . Temp ) ;
// We now have a hashmap of directed edges (vertex1 -> vertex2) such that these edges are directed the same (cw or ccw), and "outer edges".
// We can now follow these directed edges to trace out the contours of the mesh.
var toPlaneMatrix = movementPlane . AsWorldToPlaneMatrix ( ) ;
for ( int allowLoops = 0 ; allowLoops < = 1 ; allowLoops + + ) {
var minPriority = allowLoops = = 1 ? 1 : 2 ;
for ( int i = 0 ; i < obstacles . Length ; i + + ) {
if ( priority [ i ] > = minPriority ) {
int startVertices = outputVertices . Length ;
outputVertices . Add ( obstacles [ i ] . vertex1 ) ;
var lastAdded = obstacles [ i ] . vertex1 ;
var candidateVertex = obstacles [ i ] . vertex2 ;
var index = i ;
var obstacleType = ObstacleType . Chain ;
var boundsMn = lastAdded ;
var boundsMx = lastAdded ;
while ( true ) {
if ( priority [ index ] = = 0 ) {
// This should not happen for a regular navmesh.
// But it can happen if there are degenerate edges or overlapping triangles.
// In that case we will just stop here
break ;
}
priority [ index ] = 0 ;
float3 nextVertex ;
if ( traceLookup . TryGetValue ( obstacles [ index ] . vertex2LinkId , out int nextIndex ) ) {
nextVertex = 0.5f * ( obstacles [ index ] . vertex2 + obstacles [ nextIndex ] . vertex1 ) ;
} else {
nextVertex = obstacles [ index ] . vertex2 ;
nextIndex = - 1 ;
}
// Try to simplify and see if we even need to add the vertex C.
var p1 = lastAdded ;
var p2 = nextVertex ;
var p3 = candidateVertex ;
var d1 = toPlaneMatrix . ToPlane ( p2 - p1 ) ;
var d2 = toPlaneMatrix . ToPlane ( p3 - p1 ) ;
var det = VectorMath . Determinant ( d1 , d2 ) ;
if ( math . abs ( det ) < 0.01f & & simplifyObstacles ) {
// We don't need to add candidateVertex. It's just a straight line (p1,p2,p3 are colinear).
} else {
outputVertices . Add ( candidateVertex ) ;
boundsMn = math . min ( boundsMn , candidateVertex ) ;
boundsMx = math . max ( boundsMx , candidateVertex ) ;
lastAdded = p3 ;
}
if ( nextIndex = = i ) {
// Loop
outputVertices [ startVertices ] = nextVertex ;
obstacleType = ObstacleType . Loop ;
break ;
} else if ( nextIndex = = - 1 ) {
// End of chain
outputVertices . Add ( nextVertex ) ;
boundsMn = math . min ( boundsMn , nextVertex ) ;
boundsMx = math . max ( boundsMx , nextVertex ) ;
break ;
}
index = nextIndex ;
candidateVertex = nextVertex ;
}
outputMetadata . Add ( new ObstacleVertexGroup {
type = obstacleType ,
vertexCount = outputVertices . Length - startVertices ,
boundsMn = boundsMn ,
boundsMx = boundsMx ,
} ) ;
}
}
}
int obstacleSet , vertexSet ;
if ( outputMetadata . Length > 0 ) {
spinLock . Lock ( ) ;
obstacleSet = obstaclesAllocator . Allocate ( outputMetadata ) ;
vertexSet = verticesAllocator . Allocate ( outputVertices ) ;
spinLock . Unlock ( ) ;
} else {
obstacleSet = SlabAllocator < ObstacleVertexGroup > . ZeroLengthArray ;
vertexSet = SlabAllocator < float3 > . ZeroLengthArray ;
}
outputObstacles [ obstacleId ] = new UnmanagedObstacle {
verticesAllocation = vertexSet ,
groupsAllocation = obstacleSet ,
} ;
}
}
}