using UnityEngine;
using Unity.Burst;
using Unity.Collections;
using Unity.Mathematics;
using Pathfinding.Jobs;
using Pathfinding.Util;
using Pathfinding.Collections;
namespace Pathfinding.Graphs.Grid.Jobs {
///
/// Calculates the grid connections for all nodes.
///
/// This is a IJobParallelForBatch job. Calculating the connections in multiple threads is faster,
/// but due to hyperthreading (used on most intel processors) the individual threads will become slower.
/// It is still worth it though.
///
[BurstCompile(FloatMode = FloatMode.Fast, CompileSynchronously = true)]
public struct JobCalculateGridConnections : IJobParallelForBatched {
public float maxStepHeight;
public float4x4 graphToWorld;
public IntBounds bounds;
public int3 arrayBounds;
public NumNeighbours neighbours;
public float characterHeight;
public bool use2D;
public bool cutCorners;
public bool maxStepUsesSlope;
public bool layeredDataLayout;
[ReadOnly]
public UnsafeSpan nodeWalkable;
[ReadOnly]
public UnsafeSpan nodeNormals;
[ReadOnly]
public UnsafeSpan nodePositions;
/// All bitpacked node connections
[WriteOnly]
public UnsafeSpan nodeConnections;
public bool allowBoundsChecks => false;
public static bool IsValidConnection (float y, float y2, float maxStepHeight) {
return math.abs(y - y2) <= maxStepHeight;
}
public static bool IsValidConnection (float2 yRange, float2 yRange2, float maxStepHeight, float characterHeight) {
if (!IsValidConnection(yRange.x, yRange2.x, maxStepHeight)) return false;
// Find the overlap between the two spans to check if the character could pass through the vertical gap
float bottom = math.max(yRange.x, yRange2.x);
float top = math.min(yRange.y, yRange2.y);
return top-bottom >= characterHeight;
}
static float ConnectionY (UnsafeSpan nodePositions, UnsafeSpan nodeNormals, NativeArray normalToHeightOffset, int nodeIndex, int dir, float4 up, bool reverse) {
Unity.Burst.CompilerServices.Hint.Assume(nodeIndex >= 0 && nodeIndex < nodePositions.length);
Unity.Burst.CompilerServices.Hint.Assume(nodeIndex >= 0 && nodeIndex < nodeNormals.length);
Unity.Burst.CompilerServices.Hint.Assume(dir >= 0 && dir < normalToHeightOffset.Length);
float4 pos = new float4(nodePositions[(uint)nodeIndex], 0);
return math.dot(up, pos) + (reverse ? -1 : 1) * math.dot(nodeNormals[nodeIndex], normalToHeightOffset[dir]);
}
static float2 ConnectionYRange (UnsafeSpan nodePositions, UnsafeSpan nodeNormals, NativeArray normalToHeightOffset, int nodeIndex, int layerStride, int y, int maxY, int dir, float4 up, bool reverse) {
var floor = ConnectionY(nodePositions, nodeNormals, normalToHeightOffset, nodeIndex, dir, up, reverse);
float ceiling;
var aboveNodeIndex = nodeIndex + layerStride;
if ((uint)aboveNodeIndex < nodeNormals.length && math.any(nodeNormals[(uint)aboveNodeIndex])) {
ceiling = ConnectionY(nodePositions, nodeNormals, normalToHeightOffset, aboveNodeIndex, dir, up, reverse);
} else {
ceiling = float.PositiveInfinity;
}
return new float2(floor, ceiling);
}
static NativeArray HeightOffsetProjections (float4x4 graphToWorldTranform, bool maxStepUsesSlope) {
var normalToHeightOffset = new NativeArray(8, Allocator.Temp, NativeArrayOptions.ClearMemory);
if (maxStepUsesSlope) {
for (int dir = 0; dir < normalToHeightOffset.Length; dir++) {
//
// |\
// | \
// H | \ 1 _ N = Normal
// | \ _/ α |
// | α \ _/ |
// D<---|-----x ---------
// 1/2 \
// \
// .
//
// Assume we have a node at x viewed from the side, with a surface normal N.
// We want to find the height difference (H) between the node, and the point where it touches the adjacent node.
//
// This can be calculated as
// H = tan(α) * 1/2
//
// We can approximate this for small angles as:
// H = tan(α) * 1/2 ≈ sin(α) * 1/2 = N.x * 1/2
//
// This approximation is also desirable, because it doesn't allow extremely sloped nodes to connect to nodes arbitrarily far up or down.
//
// To calculate N.x, we need to take into account that the whole graph can be rotated, and it is also in 3D space.
// Instead we calculate it as N.x = -N . D (where . is the dot product, and D = flatDir is the direction to the adjacent node along the ground plane).
var flatDir = GridGraph.neighbourXOffsets[dir] * graphToWorldTranform.c0.xyz + GridGraph.neighbourZOffsets[dir] * graphToWorldTranform.c2.xyz;
// Lastly, we create a linear transform that maps any node normal to H for a given direction
// dot(normal, normalToHeightOffset[dir]) = H
normalToHeightOffset[dir] = -new float4(flatDir, 0) * 0.5f;
}
}
return normalToHeightOffset;
}
public void Execute (int start, int count) {
if (nodePositions.Length != nodeNormals.Length) throw new System.Exception("nodePositions and nodeNormals must have the same length");
if (nodePositions.Length != nodeWalkable.Length) throw new System.Exception("nodePositions and nodeWalkable must have the same length");
if (nodePositions.Length != nodeConnections.Length) throw new System.Exception("nodePositions and nodeConnections must have the same length");
if (layeredDataLayout) ExecuteLayered(start, count);
else ExecuteFlat(start, count);
}
public void ExecuteFlat (int start, int count) {
if (maxStepHeight <= 0 || use2D) maxStepHeight = float.PositiveInfinity;
float4 up = graphToWorld.c1;
NativeArray neighbourOffsets = new NativeArray(8, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < 8; i++) neighbourOffsets[i] = GridGraph.neighbourZOffsets[i] * arrayBounds.x + GridGraph.neighbourXOffsets[i];
var nodePositions = this.nodePositions.Reinterpret();
var normalToHeightOffset = HeightOffsetProjections(graphToWorld, maxStepUsesSlope);
// The loop is parallelized over z coordinates
start += bounds.min.z;
for (int z = start; z < start + count; z++) {
var initialConnections = 0xFF;
// Disable connections to out-of-bounds nodes
// See GridNode.HasConnectionInDirection
if (z == 0) initialConnections &= ~((1 << 0) | (1 << 7) | (1 << 4));
if (z == arrayBounds.z - 1) initialConnections &= ~((1 << 2) | (1 << 5) | (1 << 6));
for (int x = bounds.min.x; x < bounds.max.x; x++) {
int nodeIndex = z * arrayBounds.x + x;
if (!nodeWalkable[nodeIndex]) {
nodeConnections[nodeIndex] = 0;
continue;
}
// Bitpacked connections
// bit 0 is set if connection 0 is enabled
// bit 1 is set if connection 1 is enabled etc.
int conns = initialConnections;
// Disable connections to out-of-bounds nodes
if (x == 0) conns &= ~((1 << 3) | (1 << 6) | (1 << 7));
if (x == arrayBounds.x - 1) conns &= ~((1 << 1) | (1 << 4) | (1 << 5));
for (int dir = 0; dir < 8; dir++) {
float y = ConnectionY(nodePositions, nodeNormals, normalToHeightOffset, nodeIndex, dir, up, false);
int neighbourIndex = nodeIndex + neighbourOffsets[dir];
if ((conns & (1 << dir)) != 0) {
float y2 = ConnectionY(nodePositions, nodeNormals, normalToHeightOffset, neighbourIndex, dir, up, true);
if (!nodeWalkable[neighbourIndex] || !IsValidConnection(y, y2, maxStepHeight)) {
// Disable connection
conns &= ~(1 << dir);
}
}
}
nodeConnections[nodeIndex] = (ulong)GridNode.FilterDiagonalConnections(conns, neighbours, cutCorners);
}
}
}
public void ExecuteLayered (int start, int count) {
if (maxStepHeight <= 0 || use2D) maxStepHeight = float.PositiveInfinity;
float4 up = graphToWorld.c1;
NativeArray neighbourOffsets = new NativeArray(8, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < 8; i++) neighbourOffsets[i] = GridGraph.neighbourZOffsets[i] * arrayBounds.x + GridGraph.neighbourXOffsets[i];
var nodePositions = this.nodePositions.Reinterpret();
var normalToHeightOffset = HeightOffsetProjections(graphToWorld, maxStepUsesSlope);
var layerStride = arrayBounds.z*arrayBounds.x;
start += bounds.min.z;
for (int y = bounds.min.y; y < bounds.max.y; y++) {
// The loop is parallelized over z coordinates
for (int z = start; z < start + count; z++) {
for (int x = bounds.min.x; x < bounds.max.x; x++) {
// Bitpacked connections
ulong conns = 0;
int nodeIndexXZ = z * arrayBounds.x + x;
int nodeIndex = nodeIndexXZ + y * layerStride;
if (nodeWalkable[nodeIndex]) {
for (int dir = 0; dir < 8; dir++) {
int nx = x + GridGraph.neighbourXOffsets[dir];
int nz = z + GridGraph.neighbourZOffsets[dir];
int conn = LevelGridNode.NoConnection;
if (nx >= 0 && nz >= 0 && nx < arrayBounds.x && nz < arrayBounds.z) {
float2 yRange = ConnectionYRange(nodePositions, nodeNormals, normalToHeightOffset, nodeIndex, layerStride, y, arrayBounds.y, dir, up, false);
int neighbourStartIndex = nodeIndexXZ + neighbourOffsets[dir];
for (int y2 = 0; y2 < arrayBounds.y; y2++) {
var neighbourIndex = neighbourStartIndex + y2 * layerStride;
if (!nodeWalkable[neighbourIndex]) continue;
float2 yRange2 = ConnectionYRange(nodePositions, nodeNormals, normalToHeightOffset, neighbourIndex, layerStride, y2, arrayBounds.y, dir, up, true);
if (IsValidConnection(yRange, yRange2, maxStepHeight, characterHeight)) {
conn = y2;
break;
}
}
}
conns |= (ulong)conn << LevelGridNode.ConnectionStride*dir;
}
} else {
conns = LevelGridNode.AllConnectionsMask;
}
nodeConnections[nodeIndex] = conns;
}
}
}
}
}
}