This commit is contained in:
2026-06-11 22:49:50 +07:00
parent 458c338b27
commit e85e66002f
4105 changed files with 1435727 additions and 11 deletions

View File

@@ -0,0 +1,381 @@
// #define VALIDATE_AABB_TREE
using UnityEngine;
using System.Collections.Generic;
using Pathfinding.Util;
using Unity.Mathematics;
using UnityEngine.Assertions;
namespace Pathfinding.Collections {
/// <summary>
/// Axis Aligned Bounding Box Tree.
///
/// Holds a bounding box tree with arbitrary data.
///
/// The tree self-balances itself regularly when nodes are added.
/// </summary>
public class AABBTree<T> {
Node[] nodes = new Node[0];
int root = NoNode;
readonly Stack<int> freeNodes = new Stack<int>();
int rebuildCounter = 64;
const int NoNode = -1;
struct Node {
public Bounds bounds;
public uint flags;
const uint TagInsideBit = 1u << 30;
const uint TagPartiallyInsideBit = 1u << 31;
const uint AllocatedBit = 1u << 29;
const uint ParentMask = ~(TagInsideBit | TagPartiallyInsideBit | AllocatedBit);
public const int InvalidParent = (int)ParentMask;
public bool wholeSubtreeTagged {
get => (flags & TagInsideBit) != 0;
set => flags = (flags & ~TagInsideBit) | (value ? TagInsideBit : 0);
}
public bool subtreePartiallyTagged {
get => (flags & TagPartiallyInsideBit) != 0;
set => flags = (flags & ~TagPartiallyInsideBit) | (value ? TagPartiallyInsideBit : 0);
}
public bool isAllocated {
get => (flags & AllocatedBit) != 0;
set => flags = (flags & ~AllocatedBit) | (value ? AllocatedBit : 0);
}
public bool isLeaf => left == NoNode;
public int parent {
get => (int)(flags & ParentMask);
set => flags = (flags & ~ParentMask) | (uint)value;
}
public int left;
public int right;
public T value;
}
/// <summary>A key to a leaf node in the tree</summary>
public readonly struct Key {
internal readonly int value;
public int node => value - 1;
public bool isValid => value != 0;
internal Key(int node) { this.value = node + 1; }
}
static float ExpansionRequired (Bounds b, Bounds b2) {
var union = b;
union.Encapsulate(b2);
return union.size.x*union.size.y*union.size.z - b.size.x*b.size.y*b.size.z;
}
/// <summary>User data for a node in the tree</summary>
public T this[Key key] => nodes[key.node].value;
/// <summary>Bounding box of a given node</summary>
public Bounds GetBounds (Key key) {
if (!key.isValid) throw new System.ArgumentException("Key is not valid");
var node = nodes[key.node];
if (!node.isAllocated) throw new System.ArgumentException("Key does not point to an allocated node");
if (!node.isLeaf) throw new System.ArgumentException("Key does not point to a leaf node");
return node.bounds;
}
int AllocNode () {
if (!freeNodes.TryPop(out int newNodeId)) {
int prevLength = nodes.Length;
Memory.Realloc(ref nodes, Mathf.Max(8, nodes.Length*2));
for (int i = nodes.Length - 1; i >= prevLength; i--) FreeNode(i);
newNodeId = freeNodes.Pop();
#if VALIDATE_AABB_TREE
Assert.IsFalse(nodes[newNodeId].isAllocated);
#endif
}
return newNodeId;
}
void FreeNode (int node) {
nodes[node].isAllocated = false;
nodes[node].value = default;
freeNodes.Push(node);
}
/// <summary>
/// Rebuilds the whole tree.
///
/// This can make it more balanced, and thus faster to query.
/// </summary>
public void Rebuild () {
var leaves = new UnsafeSpan<int>(Unity.Collections.Allocator.Temp, nodes.Length);
int nLeaves = 0;
for (int i = 0; i < nodes.Length; i++) {
if (!nodes[i].isAllocated) continue;
if (nodes[i].isLeaf) leaves[nLeaves++] = i;
else FreeNode(i);
}
root = Rebuild(leaves.Slice(0, nLeaves), Node.InvalidParent);
rebuildCounter = Mathf.Max(64, nLeaves / 3);
Validate(root);
}
/// <summary>Removes all nodes from the tree</summary>
public void Clear () {
for (int i = 0; i < nodes.Length; i++) {
if (nodes[i].isAllocated) FreeNode(i);
}
root = NoNode;
rebuildCounter = 64;
}
struct AABBComparer : IComparer<int> {
public Node[] nodes;
public int dim;
public int Compare(int a, int b) => nodes[a].bounds.center[dim].CompareTo(nodes[b].bounds.center[dim]);
}
static int ArgMax (Vector3 v) {
var m = Mathf.Max(v.x, Mathf.Max(v.y, v.z));
return m == v.x ? 0: (m == v.y ? 1 : 2);
}
int Rebuild (UnsafeSpan<int> leaves, int parent) {
if (leaves.Length == 0) return NoNode;
if (leaves.Length == 1) {
nodes[leaves[0]].parent = parent;
return leaves[0];
}
var bounds = nodes[leaves[0]].bounds;
for (int i = 1; i < leaves.Length; i++) bounds.Encapsulate(nodes[leaves[i]].bounds);
leaves.Sort(new AABBComparer { nodes = nodes, dim = ArgMax(bounds.extents) });
var nodeId = AllocNode();
nodes[nodeId] = new Node {
bounds = bounds,
left = Rebuild(leaves.Slice(0, leaves.Length/2), nodeId),
right = Rebuild(leaves.Slice(leaves.Length/2), nodeId),
parent = parent,
isAllocated = true,
};
return nodeId;
}
/// <summary>
/// Moves a node to a new position.
///
/// This will update the tree structure to account for the new bounding box.
/// This is equivalent to removing the node and adding it again with the new bounds, but it preserves the key value.
/// </summary>
/// <param name="key">Key to the node to move</param>
/// <param name="bounds">New bounds of the node</param>
public void Move (Key key, Bounds bounds) {
var value = nodes[key.node].value;
Remove(key);
var newKey = Add(bounds, value);
// The first node added after a remove will have the same node index as the just removed node
Assert.IsTrue(newKey.node == key.node);
}
[System.Diagnostics.Conditional("VALIDATE_AABB_TREE")]
void Validate (int node) {
if (node == NoNode) return;
var n = nodes[node];
Assert.IsTrue(n.isAllocated);
if (node == root) {
Assert.AreEqual(Node.InvalidParent, n.parent);
} else {
Assert.AreNotEqual(Node.InvalidParent, n.parent);
}
if (n.isLeaf) {
Assert.AreEqual(NoNode, n.right);
} else {
Assert.AreNotEqual(NoNode, n.right);
Assert.AreNotEqual(n.left, n.right);
Assert.AreEqual(node, nodes[n.left].parent);
Assert.AreEqual(node, nodes[n.right].parent);
Assert.IsTrue(math.all((float3)n.bounds.min <= (float3)nodes[n.left].bounds.min + 0.0001f));
Assert.IsTrue(math.all((float3)n.bounds.max >= (float3)nodes[n.left].bounds.max - 0.0001f));
Assert.IsTrue(math.all((float3)n.bounds.min <= (float3)nodes[n.right].bounds.min + 0.0001f));
Assert.IsTrue(math.all((float3)n.bounds.max >= (float3)nodes[n.right].bounds.max - 0.0001f));
Validate(n.left);
Validate(n.right);
}
}
public Bounds Remove (Key key) {
if (!key.isValid) throw new System.ArgumentException("Key is not valid");
var node = nodes[key.node];
if (!node.isAllocated) throw new System.ArgumentException("Key does not point to an allocated node");
if (!node.isLeaf) throw new System.ArgumentException("Key does not point to a leaf node");
if (key.node == root) {
root = NoNode;
FreeNode(key.node);
return node.bounds;
}
// Remove the parent from the tree and replace it with sibling
var parentToRemoveId = node.parent;
var parentToRemove = nodes[parentToRemoveId];
var siblingId = parentToRemove.left == key.node ? parentToRemove.right : parentToRemove.left;
FreeNode(parentToRemoveId);
FreeNode(key.node);
nodes[siblingId].parent = parentToRemove.parent;
if (parentToRemove.parent == Node.InvalidParent) {
root = siblingId;
} else {
if (nodes[parentToRemove.parent].left == parentToRemoveId) {
nodes[parentToRemove.parent].left = siblingId;
} else {
nodes[parentToRemove.parent].right = siblingId;
}
}
// Rebuild bounding boxes
var tmpNodeId = nodes[siblingId].parent;
while (tmpNodeId != Node.InvalidParent) {
ref var tmpNode = ref nodes[tmpNodeId];
var bounds = nodes[tmpNode.left].bounds;
bounds.Encapsulate(nodes[tmpNode.right].bounds);
tmpNode.bounds = bounds;
tmpNode.subtreePartiallyTagged = nodes[tmpNode.left].subtreePartiallyTagged | nodes[tmpNode.right].subtreePartiallyTagged;
tmpNodeId = tmpNode.parent;
}
Validate(root);
return node.bounds;
}
public Key Add (Bounds bounds, T value) {
var newNodeId = AllocNode();
nodes[newNodeId] = new Node {
bounds = bounds,
parent = Node.InvalidParent,
left = NoNode,
right = NoNode,
value = value,
isAllocated = true,
};
if (root == NoNode) {
root = newNodeId;
Validate(root);
return new Key(newNodeId);
}
int nodeId = root;
while (true) {
var node = nodes[nodeId];
// We can no longer guarantee that the whole subtree of this node is tagged,
// as the new node is not tagged
nodes[nodeId].wholeSubtreeTagged = false;
if (node.isLeaf) {
var newInnerId = AllocNode();
if (node.parent != Node.InvalidParent) {
if (nodes[node.parent].left == nodeId) nodes[node.parent].left = newInnerId;
else nodes[node.parent].right = newInnerId;
}
bounds.Encapsulate(node.bounds);
nodes[newInnerId] = new Node {
bounds = bounds,
left = nodeId,
right = newNodeId,
parent = node.parent,
isAllocated = true,
};
nodes[newNodeId].parent = nodes[nodeId].parent = newInnerId;
if (root == nodeId) root = newInnerId;
if (rebuildCounter-- <= 0) Rebuild();
Validate(root);
return new Key(newNodeId);
} else {
// Inner node
nodes[nodeId].bounds.Encapsulate(bounds);
float leftCost = ExpansionRequired(nodes[node.left].bounds, bounds);
float rightCost = ExpansionRequired(nodes[node.right].bounds, bounds);
nodeId = leftCost < rightCost ? node.left : node.right;
}
}
}
/// <summary>Queries the tree for all objects that touch the specified bounds.</summary>
/// <param name="bounds">Bounding box to search within</param>
/// <param name="buffer">The results will be added to the buffer</param>
public void Query(Bounds bounds, List<T> buffer) => QueryNode(root, bounds, buffer);
void QueryNode (int node, Bounds bounds, List<T> buffer) {
if (node == NoNode || !bounds.Intersects(nodes[node].bounds)) return;
if (nodes[node].isLeaf) {
buffer.Add(nodes[node].value);
} else {
// Search children
QueryNode(nodes[node].left, bounds, buffer);
QueryNode(nodes[node].right, bounds, buffer);
}
}
/// <summary>Queries the tree for all objects that have been previously tagged using the <see cref="Tag"/> method.</summary>
/// <param name="buffer">The results will be added to the buffer</param>
/// <param name="clearTags">If true, all tags will be cleared after this call. If false, the tags will remain and can be queried again later.</param>
public void QueryTagged(List<T> buffer, bool clearTags = false) => QueryTaggedNode(root, clearTags, buffer);
void QueryTaggedNode (int node, bool clearTags, List<T> buffer) {
if (node == NoNode || !nodes[node].subtreePartiallyTagged) return;
if (clearTags) {
nodes[node].wholeSubtreeTagged = false;
nodes[node].subtreePartiallyTagged = false;
}
if (nodes[node].isLeaf) {
buffer.Add(nodes[node].value);
} else {
QueryTaggedNode(nodes[node].left, clearTags, buffer);
QueryTaggedNode(nodes[node].right, clearTags, buffer);
}
}
/// <summary>
/// Tags a particular object.
///
/// Any previously tagged objects stay tagged.
/// You can retrieve the tagged objects using the <see cref="QueryTagged"/> method.
/// </summary>
/// <param name="key">Key to the object to tag</param>
public void Tag (Key key) {
if (!key.isValid) throw new System.ArgumentException("Key is not valid");
if (key.node < 0 || key.node >= nodes.Length) throw new System.ArgumentException("Key does not point to a valid node");
ref var node = ref nodes[key.node];
if (!node.isAllocated) throw new System.ArgumentException("Key does not point to an allocated node");
if (!node.isLeaf) throw new System.ArgumentException("Key does not point to a leaf node");
node.wholeSubtreeTagged = true;
int nodeId = key.node;
while (nodeId != Node.InvalidParent) {
nodes[nodeId].subtreePartiallyTagged = true;
nodeId = nodes[nodeId].parent;
}
}
/// <summary>
/// Tags all objects that touch the specified bounds.
///
/// Any previously tagged objects stay tagged.
/// You can retrieve the tagged objects using the <see cref="QueryTagged"/> method.
/// </summary>
/// <param name="bounds">Bounding box to search within</param>
public void Tag(Bounds bounds) => TagNode(root, bounds);
bool TagNode (int node, Bounds bounds) {
if (node == NoNode || nodes[node].wholeSubtreeTagged) return true; // Nothing to do
if (!bounds.Intersects(nodes[node].bounds)) return false;
// TODO: Could make this less conservative by propagating info from the child nodes
nodes[node].subtreePartiallyTagged = true;
if (nodes[node].isLeaf) return nodes[node].wholeSubtreeTagged = true;
else return nodes[node].wholeSubtreeTagged = TagNode(nodes[node].left, bounds) & TagNode(nodes[node].right, bounds);
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 183e10f9cadca424792b5f940ce3fe3d
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,578 @@
//#define ASTARDEBUG //"BBTree Debug" If enables, some queries to the tree will show debug lines. Turn off multithreading when using this since DrawLine calls cannot be called from a different thread
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Pathfinding.Drawing;
namespace Pathfinding.Collections {
using Pathfinding.Util;
/// <summary>
/// Axis Aligned Bounding Box Tree.
/// Holds a bounding box tree of triangles.
/// </summary>
[BurstCompile]
public struct BBTree : IDisposable {
/// <summary>Holds all tree nodes</summary>
UnsafeList<BBTreeBox> tree;
UnsafeList<int> nodePermutation;
const int MaximumLeafSize = 4;
public IntRect Size => tree.Length == 0 ? default : tree[0].rect;
// We need a stack while searching the tree.
// We use a stack allocated array for this to avoid allocations.
// A tile can at most contain NavmeshBase.VertexIndexMask triangles.
// This works out to about a million. A perfectly balanced tree can fit this in log2(1000000/4) = 18 levels.
// but we add a few more levels just to be safe, in case the tree is not perfectly balanced.
const int MAX_TREE_HEIGHT = 26;
public void Dispose () {
nodePermutation.Dispose();
tree.Dispose();
}
/// <summary>Build a BBTree from a list of triangles.</summary>
/// <param name="triangles">The triangles. Each triplet of 3 indices represents a node. The triangles are assumed to be in clockwise order.</param>
/// <param name="vertices">The vertices of the triangles.</param>
public BBTree(UnsafeSpan<int> triangles, UnsafeSpan<Int3> vertices) {
if (triangles.Length % 3 != 0) throw new ArgumentException("triangles must be a multiple of 3 in length");
Build(ref triangles, ref vertices, out this);
}
[BurstCompile]
static void Build (ref UnsafeSpan<int> triangles, ref UnsafeSpan<Int3> vertices, out BBTree bbTree) {
var nodeCount = triangles.Length/3;
// We will use approximately 2N tree nodes
var tree = new UnsafeList<BBTreeBox>((int)(nodeCount * 2.1f), Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
// We will use approximately N node references
var nodes = new UnsafeList<int>((int)(nodeCount * 1.1f), Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
// This will store the order of the nodes while the tree is being built
// It turns out that it is a lot faster to do this than to actually modify
// the nodes and nodeBounds arrays (presumably since that involves shuffling
// around 20 bytes of memory (sizeof(pointer) + sizeof(IntRect)) per node
// instead of 4 bytes (sizeof(int)).
// It also means we don't have to make a copy of the nodes array since
// we do not modify it
var permutation = new NativeArray<int>(nodeCount, Allocator.Temp);
for (int i = 0; i < nodeCount; i++) {
permutation[i] = i;
}
// Precalculate the bounds of the nodes in XZ space.
// It turns out that calculating the bounds is a bottleneck and precalculating
// the bounds makes it around 3 times faster to build a tree
var nodeBounds = new NativeArray<IntRect>(nodeCount, Allocator.Temp);
for (int i = 0; i < nodeCount; i++) {
var v0 = ((int3)vertices[triangles[i*3+0]]).xz;
var v1 = ((int3)vertices[triangles[i*3+1]]).xz;
var v2 = ((int3)vertices[triangles[i*3+2]]).xz;
var mn = math.min(v0, math.min(v1, v2));
var mx = math.max(v0, math.max(v1, v2));
nodeBounds[i] = new IntRect(mn.x, mn.y, mx.x, mx.y);
}
if (nodeCount > 0) BuildSubtree(permutation, nodeBounds, ref nodes, ref tree, 0, nodeCount, false, 0);
nodeBounds.Dispose();
permutation.Dispose();
bbTree = new BBTree {
tree = tree,
nodePermutation = nodes,
};
}
static int SplitByX (NativeArray<IntRect> nodesBounds, NativeArray<int> permutation, int from, int to, int divider) {
int mx = to;
for (int i = from; i < mx; i++) {
var cr = nodesBounds[permutation[i]];
var cx = (cr.xmin + cr.xmax)/2;
if (cx > divider) {
mx--;
// Swap items i and mx
var tmp = permutation[mx];
permutation[mx] = permutation[i];
permutation[i] = tmp;
i--;
}
}
return mx;
}
static int SplitByZ (NativeArray<IntRect> nodesBounds, NativeArray<int> permutation, int from, int to, int divider) {
int mx = to;
for (int i = from; i < mx; i++) {
var cr = nodesBounds[permutation[i]];
var cx = (cr.ymin + cr.ymax)/2;
if (cx > divider) {
mx--;
// Swap items i and mx
var tmp = permutation[mx];
permutation[mx] = permutation[i];
permutation[i] = tmp;
i--;
}
}
return mx;
}
static int BuildSubtree (NativeArray<int> permutation, NativeArray<IntRect> nodeBounds, ref UnsafeList<int> nodes, ref UnsafeList<BBTreeBox> tree, int from, int to, bool odd, int depth) {
var rect = NodeBounds(permutation, nodeBounds, from, to);
int boxId = tree.Length;
tree.Add(new BBTreeBox(rect));
if (to - from <= MaximumLeafSize) {
if (depth > MAX_TREE_HEIGHT) {
Debug.LogWarning($"Maximum tree height of {MAX_TREE_HEIGHT} exceeded (got depth of {depth}). Querying this tree may fail. Is the tree very unbalanced?");
}
var box = tree[boxId];
var nodeOffset = box.nodeOffset = nodes.Length;
tree[boxId] = box;
nodes.Length += MaximumLeafSize;
// Assign all nodes to the array. Note that we also need clear unused slots as the array from the pool may contain any information
for (int i = 0; i < MaximumLeafSize; i++) {
nodes[nodeOffset + i] = i < to - from ? permutation[from + i] : -1;
}
return boxId;
} else {
int splitIndex;
if (odd) {
// X
int divider = (rect.xmin + rect.xmax)/2;
splitIndex = SplitByX(nodeBounds, permutation, from, to, divider);
} else {
// Y/Z
int divider = (rect.ymin + rect.ymax)/2;
splitIndex = SplitByZ(nodeBounds, permutation, from, to, divider);
}
int margin = (to - from)/8;
bool veryUneven = splitIndex <= from + margin || splitIndex >= to - margin;
if (veryUneven) {
// All nodes were on one side of the divider
// Try to split along the other axis
if (!odd) {
// X
int divider = (rect.xmin + rect.xmax)/2;
splitIndex = SplitByX(nodeBounds, permutation, from, to, divider);
} else {
// Y/Z
int divider = (rect.ymin + rect.ymax)/2;
splitIndex = SplitByZ(nodeBounds, permutation, from, to, divider);
}
veryUneven = splitIndex <= from + margin || splitIndex >= to - margin;
if (veryUneven) {
// Almost all nodes were on one side of the divider
// Just pick one half
splitIndex = (from+to)/2;
}
}
var left = BuildSubtree(permutation, nodeBounds, ref nodes, ref tree, from, splitIndex, !odd, depth+1);
var right = BuildSubtree(permutation, nodeBounds, ref nodes, ref tree, splitIndex, to, !odd, depth+1);
var box = tree[boxId];
box.left = left;
box.right = right;
tree[boxId] = box;
return boxId;
}
}
/// <summary>Calculates the bounding box in XZ space of all nodes between from (inclusive) and to (exclusive)</summary>
static IntRect NodeBounds (NativeArray<int> permutation, NativeArray<IntRect> nodeBounds, int from, int to) {
var mn = nodeBounds[permutation[from]].Min.ToInt2();
var mx = nodeBounds[permutation[from]].Max.ToInt2();
for (int j = from + 1; j < to; j++) {
var otherRect = nodeBounds[permutation[j]];
var rmin = new int2(otherRect.xmin, otherRect.ymin);
var rmax = new int2(otherRect.xmax, otherRect.ymax);
mn = math.min(mn, rmin);
mx = math.max(mx, rmax);
}
return new IntRect(mn.x, mn.y, mx.x, mx.y);
}
[BurstCompile]
public readonly struct ProjectionParams {
public readonly float2x3 planeProjection;
public readonly float2 projectedUpNormalized;
public readonly float3 projectionAxis;
public readonly float distanceScaleAlongProjectionAxis;
public readonly DistanceMetric distanceMetric;
// bools are for some reason not blittable by the burst compiler, so we have to use a byte
readonly byte alignedWithXZPlaneBacking;
public bool alignedWithXZPlane => alignedWithXZPlaneBacking != 0;
/// <summary>
/// Calculates the squared distance from a point to a box when projected to 2D.
///
/// The input rectangle is assumed to be on the XZ plane, and to actually represent an infinitely tall box (along the Y axis).
///
/// The planeProjection matrix projects points from 3D to 2D. The box will also be projected.
/// The upProjNormalized vector is the normalized direction orthogonal to the 2D projection.
/// It is the direction pointing out of the plane from the projection's point of view.
///
/// In the special case that the projection just projects 3D coordinates onto the XZ plane, this is
/// equivalent to the distance from a point to a rectangle in 2D.
/// </summary>
public float SquaredRectPointDistanceOnPlane (IntRect rect, float3 p) {
return SquaredRectPointDistanceOnPlane(in this, ref rect, ref p);
}
[BurstCompile(FloatMode = FloatMode.Fast)]
private static float SquaredRectPointDistanceOnPlane (in ProjectionParams projection, ref IntRect rect, ref float3 p) {
if (projection.alignedWithXZPlane) {
var p1 = new float2(rect.xmin, rect.ymin) * Int3.PrecisionFactor;
var p4 = new float2(rect.xmax, rect.ymax) * Int3.PrecisionFactor;
var closest = math.clamp(p.xz, p1, p4);
return math.lengthsq(closest - p.xz);
} else {
var p1 = new float3(rect.xmin, 0, rect.ymin) * Int3.PrecisionFactor - p;
var p4 = new float3(rect.xmax, 0, rect.ymax) * Int3.PrecisionFactor - p;
var p2 = new float3(rect.xmin, 0, rect.ymax) * Int3.PrecisionFactor - p;
var p3 = new float3(rect.xmax, 0, rect.ymin) * Int3.PrecisionFactor - p;
var p1proj = math.mul(projection.planeProjection, p1);
var p2proj = math.mul(projection.planeProjection, p2);
var p3proj = math.mul(projection.planeProjection, p3);
var p4proj = math.mul(projection.planeProjection, p4);
var upNormal = new float2(projection.projectedUpNormalized.y, -projection.projectedUpNormalized.x);
// Calculate the dot product of pNproj and upNormal for all N, this is the distance between p and pN
// along the direction orthogonal to upProjNormalized.
// The box is infinite along the up direction (since it is only a rect). When projected down to 2D
// this results in an infinite line with a given thickness (a beam).
// This is assuming the projection direction is not parallel to the world up direction, in which case we
// would have entered the other branch of this if statement.
// The minumum value and maximum value in dists gives us the signed distance to this beam
// from the point p.
var dists = math.mul(math.transpose(new float2x4(p1proj, p2proj, p3proj, p4proj)), upNormal);
// Calculate the shortest distance to the beam (may be 0 if p is inside the beam).
var dist = math.clamp(0, math.cmin(dists), math.cmax(dists));
return dist*dist;
}
}
public ProjectionParams(NNConstraint constraint, GraphTransform graphTransform) {
const float MAX_ERROR_IN_RADIANS = 0.01f;
// The normal of the plane we are projecting onto (if any).
if (constraint != null && constraint.distanceMetric.projectionAxis != Vector3.zero) {
// (inf,inf,inf) is a special value indicating to use the graph's natural up direction
if (float.IsPositiveInfinity(constraint.distanceMetric.projectionAxis.x)) {
projectionAxis = new float3(0, 1, 0);
} else {
projectionAxis = math.normalizesafe(graphTransform.InverseTransformVector(constraint.distanceMetric.projectionAxis));
}
if (projectionAxis.x*projectionAxis.x + projectionAxis.z*projectionAxis.z < MAX_ERROR_IN_RADIANS*MAX_ERROR_IN_RADIANS) {
// We could let the code below handle this case, but since it is a common case we can optimize it a bit
// by using a fast-path here.
projectedUpNormalized = float2.zero;
planeProjection = new float2x3(1, 0, 0, 0, 0, 1); // math.transpose(new float3x2(new float3(1, 0, 0), new float3(0, 0, 1)));
distanceMetric = DistanceMetric.ScaledManhattan;
alignedWithXZPlaneBacking = (byte)1;
distanceScaleAlongProjectionAxis = math.max(constraint.distanceMetric.distanceScaleAlongProjectionDirection, 0);
return;
}
// Find any two vectors which are perpendicular to the normal (and each other)
var planeAxis1 = math.normalizesafe(math.cross(new float3(1, 0, 1), projectionAxis));
if (math.all(planeAxis1 == 0)) planeAxis1 = math.normalizesafe(math.cross(new float3(-1, 0, 1), projectionAxis));
var planeAxis2 = math.normalizesafe(math.cross(projectionAxis, planeAxis1));
// Note: The inverse of an orthogonal matrix is its transpose, and the transpose is faster to compute
planeProjection = math.transpose(new float3x2(planeAxis1, planeAxis2));
// The projection of the (0,1,0) vector onto the plane.
// This is important because the BBTree stores its rectangles in the XZ plane.
// If the projection is close enough to the XZ plane, we snap to that because it allows us to use faster and more precise distance calculations.
projectedUpNormalized = math.lengthsq(planeProjection.c1) <= MAX_ERROR_IN_RADIANS*MAX_ERROR_IN_RADIANS ? float2.zero : math.normalize(planeProjection.c1);
distanceMetric = DistanceMetric.ScaledManhattan;
alignedWithXZPlaneBacking = math.all(projectedUpNormalized == 0) ? (byte)1 : (byte)0;
// The distance along the projection axis is scaled by a cost factor to make the distance
// along the projection direction more or less important compared to the distance in the plane.
// Usually the projection direction is less important.
// For example, when an agent looks for the closest node, it is typically more interested in finding a point close
// to it which is more or less directly below it, than it is in finding a point which is closer, but requires sideways movement.
// Even if this value is zero we will use the distance along the projection axis to break ties.
// Otherwise, when getting the nearest node in e.g. a tall building, it would not be well defined
// which floor of the building was closest.
distanceScaleAlongProjectionAxis = math.max(constraint.distanceMetric.distanceScaleAlongProjectionDirection, 0);
} else {
projectionAxis = float3.zero;
planeProjection = default;
projectedUpNormalized = default;
distanceMetric = DistanceMetric.Euclidean;
alignedWithXZPlaneBacking = 1;
distanceScaleAlongProjectionAxis = 0;
}
}
}
public float DistanceSqrLowerBound (float3 p, in ProjectionParams projection) {
if (tree.Length == 0) return float.PositiveInfinity;
return projection.SquaredRectPointDistanceOnPlane(tree[0].rect, p);
}
/// <summary>
/// Queries the tree for the closest node to p constrained by the NNConstraint trying to improve an existing solution.
/// Note that this function will only fill in the constrained node.
/// If you want a node not constrained by any NNConstraint, do an additional search with constraint = NNConstraint.None
/// </summary>
/// <param name="p">Point to search around</param>
/// <param name="constraint">Optionally set to constrain which nodes to return</param>
/// <param name="distanceSqr">The best squared distance for the previous solution. Will be updated with the best distance
/// after this search. Supply positive infinity to start the search from scratch.</param>
/// <param name="previous">This search will start from the previous NNInfo and improve it if possible. Will be updated with the new result.
/// Even if the search fails on this call, the solution will never be worse than previous.</param>
/// <param name="nodes">The nodes what this BBTree was built from</param>
/// <param name="triangles">The triangles that this BBTree was built from</param>
/// <param name="vertices">The vertices that this BBTree was built from</param>
/// <param name="projection">Projection parameters derived from the constraint</param>
public void QueryClosest (float3 p, NNConstraint constraint, in ProjectionParams projection, ref float distanceSqr, ref NNInfo previous, GraphNode[] nodes, UnsafeSpan<int> triangles, UnsafeSpan<Int3> vertices) {
if (tree.Length == 0) return;
UnsafeSpan<NearbyNodesIterator.BoxWithDist> stack;
unsafe {
NearbyNodesIterator.BoxWithDist* stackPtr = stackalloc NearbyNodesIterator.BoxWithDist[MAX_TREE_HEIGHT];
stack = new UnsafeSpan<NearbyNodesIterator.BoxWithDist>(stackPtr, MAX_TREE_HEIGHT);
}
stack[0] = new NearbyNodesIterator.BoxWithDist {
index = 0,
distSqr = 0.0f,
};
var it = new NearbyNodesIterator {
stack = stack,
stackSize = 1,
indexInLeaf = 0,
point = p,
projection = projection,
distanceThresholdSqr = distanceSqr,
tieBreakingDistanceThreshold = float.PositiveInfinity,
tree = tree.AsUnsafeSpan(),
nodes = nodePermutation.AsUnsafeSpan(),
triangles = triangles,
vertices = vertices,
};
// We use an iterator which searches through the tree and returns nodes closer than it.distanceThresholdSqr.
// The iterator is compiled using burst for high performance, but when a new candidate node is found we need
// to evaluate it in pure C# due to the NNConstraint being a C# class.
// TODO: If constraint==null (or NNConstraint.None) we could run the whole thing in burst to improve perf even more.
var result = previous;
while (it.stackSize > 0 && it.MoveNext()) {
var current = it.current;
if (constraint == null || constraint.Suitable(nodes[current.node])) {
it.distanceThresholdSqr = current.distanceSq;
it.tieBreakingDistanceThreshold = current.tieBreakingDistance;
result = new NNInfo(nodes[current.node], current.closestPointOnNode, current.distanceSq);
}
}
distanceSqr = it.distanceThresholdSqr;
previous = result;
}
struct CloseNode {
public int node;
public float distanceSq;
public float tieBreakingDistance;
public float3 closestPointOnNode;
}
public enum DistanceMetric: byte {
Euclidean,
ScaledManhattan,
}
[BurstCompile]
struct NearbyNodesIterator : IEnumerator<CloseNode> {
public UnsafeSpan<BoxWithDist> stack;
public int stackSize;
public UnsafeSpan<BBTreeBox> tree;
public UnsafeSpan<int> nodes;
public UnsafeSpan<int> triangles;
public UnsafeSpan<Int3> vertices;
public int indexInLeaf;
public float3 point;
public ProjectionParams projection;
public float distanceThresholdSqr;
public float tieBreakingDistanceThreshold;
internal CloseNode current;
public CloseNode Current => current;
public struct BoxWithDist {
public int index;
public float distSqr;
}
public bool MoveNext () {
return MoveNext(ref this);
}
void IDisposable.Dispose () {}
void System.Collections.IEnumerator.Reset() => throw new NotSupportedException();
object System.Collections.IEnumerator.Current => throw new NotSupportedException();
// Note: Using FloatMode=Fast here can cause NaNs in rare cases.
// I have not tracked down why, but it is not unreasonable given that FloatMode=Fast assumes that infinities do not happen.
[BurstCompile(FloatMode = FloatMode.Default)]
static bool MoveNext (ref NearbyNodesIterator it) {
var distanceThresholdSqr = it.distanceThresholdSqr;
while (true) {
if (it.stackSize == 0) {
return false;
}
// Pop the last element from the stack
var boxRef = it.stack[it.stackSize-1];
// If we cannot possibly find anything better than the current best solution in here, skip this box.
// Allow the search when we can find an equally close node, because tie breaking
// may cause this search to find a better node.
if (boxRef.distSqr > distanceThresholdSqr) {
it.stackSize--;
// Setting this to zero shouldn't be necessary in theory, as a leaf will always (in theory) be searched completely.
// However, in practice the distance to a node may be a tiny bit lower than the distance to the box containing the node, due to floating point errors.
// and so the leaf's search may be terminated early if a point is found on a node exactly on the border of the box.
// In that case it is important that we reset the iterator to the start of the next leaf.
it.indexInLeaf = 0;
continue;
}
BBTreeBox box = it.tree[boxRef.index];
if (box.IsLeaf) {
for (int i = it.indexInLeaf; i < MaximumLeafSize; i++) {
var node = it.nodes[box.nodeOffset + i];
if (node == -1) break;
var ti1 = (uint)(node*3 + 0);
var ti2 = (uint)(node*3 + 1);
var ti3 = (uint)(node*3 + 2);
if (ti3 >= it.triangles.length) throw new Exception("Invalid node index");
Unity.Burst.CompilerServices.Hint.Assume(ti1 < it.triangles.length && ti2 < it.triangles.length && ti3 < it.triangles.length);
var vi1 = it.vertices[it.triangles[ti1]];
var vi2 = it.vertices[it.triangles[ti2]];
var vi3 = it.vertices[it.triangles[ti3]];
if (it.projection.distanceMetric == DistanceMetric.Euclidean) {
var v1 = (float3)vi1;
var v2 = (float3)vi2;
var v3 = (float3)vi3;
Polygon.ClosestPointOnTriangleByRef(in v1, in v2, in v3, in it.point, out var closest);
var sqrDist = math.distancesq(closest, it.point);
if (sqrDist < distanceThresholdSqr) {
it.indexInLeaf = i + 1;
it.current = new CloseNode {
node = node,
distanceSq = sqrDist,
tieBreakingDistance = 0,
closestPointOnNode = closest,
};
return true;
}
} else {
Polygon.ClosestPointOnTriangleProjected(ref vi1, ref vi2, ref vi3, ref it.projection, ref it.point, out var closest, out var sqrDist, out var distAlongProjection);
// Check if this point is better than the previously best point.
// Handling ties here is important, in case the navmesh has multiple overlapping regions (e.g. a multi-story building).
if (sqrDist < distanceThresholdSqr || (sqrDist == distanceThresholdSqr && distAlongProjection < it.tieBreakingDistanceThreshold)) {
it.indexInLeaf = i + 1;
it.current = new CloseNode {
node = node,
distanceSq = sqrDist,
tieBreakingDistance = distAlongProjection,
closestPointOnNode = closest,
};
return true;
}
}
}
it.indexInLeaf = 0;
it.stackSize--;
} else {
it.stackSize--;
int first = box.left, second = box.right;
var firstDist = it.projection.SquaredRectPointDistanceOnPlane(it.tree[first].rect, it.point);
var secondDist = it.projection.SquaredRectPointDistanceOnPlane(it.tree[second].rect, it.point);
if (secondDist < firstDist) {
// Swap
Memory.Swap(ref first, ref second);
Memory.Swap(ref firstDist, ref secondDist);
}
if (it.stackSize + 2 > it.stack.Length) {
throw new InvalidOperationException("Tree is too deep. Overflowed the internal stack.");
}
// Push both children on the stack so that we can explore them later (if they are not too far away).
// We push the one with the smallest distance last so that it will be popped first.
if (secondDist <= distanceThresholdSqr) it.stack[it.stackSize++] = new BoxWithDist {
index = second,
distSqr = secondDist,
};
if (firstDist <= distanceThresholdSqr) it.stack[it.stackSize++] = new BoxWithDist {
index = first,
distSqr = firstDist,
};
}
}
}
}
struct BBTreeBox {
public IntRect rect;
public int nodeOffset;
public int left, right;
public bool IsLeaf => nodeOffset >= 0;
public BBTreeBox (IntRect rect) {
nodeOffset = -1;
this.rect = rect;
left = right = -1;
}
}
public void DrawGizmos (CommandBuilder draw) {
Gizmos.color = new Color(1, 1, 1, 0.5F);
if (tree.Length == 0) return;
DrawGizmos(ref draw, 0, 0);
}
void DrawGizmos (ref CommandBuilder draw, int boxi, int depth) {
BBTreeBox box = tree[boxi];
var min = (Vector3) new Int3(box.rect.xmin, 0, box.rect.ymin);
var max = (Vector3) new Int3(box.rect.xmax, 0, box.rect.ymax);
Vector3 center = (min+max)*0.5F;
Vector3 size = max-min;
size = new Vector3(size.x, 1, size.z);
center.y += depth * 2;
draw.xz.WireRectangle(center, new float2(size.x, size.z), AstarMath.IntToColor(depth, 1f));
if (!box.IsLeaf) {
DrawGizmos(ref draw, box.left, depth + 1);
DrawGizmos(ref draw, box.right, depth + 1);
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3a20480c673fd40a5bd2a4cc2206dbc4
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,352 @@
using UnityEngine;
using System.Collections.Generic;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Burst;
using UnityEngine.Assertions;
using UnityEngine.Profiling;
using Pathfinding.Util;
using Pathfinding.Collections;
using UnityEngine.Tilemaps;
namespace Pathfinding.Graphs.Navmesh {
[BurstCompile]
struct CircleGeometryUtilities {
/// <summary>
/// Cached values for CircleRadiusAdjustmentFactor.
///
/// We can calculate the area of a polygonized circle, and equate that with the area of a unit circle
/// <code>
/// x * cos(math.PI / steps) * sin(math.PI/steps) * steps = pi
/// </code>
/// Solving for the factor that makes them equal (x) gives the expression below.
///
/// Generated using the python code:
/// <code>
/// [math.sqrt(2 * math.pi / (i * math.sin(2 * math.pi / i))) for i in range(3, 23)]
/// </code>
///
/// It would be nice to generate this using a static constructor, but that is not supported by Unity's burst compiler.
/// </summary>
static readonly float[] circleRadiusAdjustmentFactors = new float[] {
1.56f, 1.25f, 1.15f, 1.1f, 1.07f, 1.05f, 1.04f, 1.03f, 1.03f, 1.02f, 1.02f, 1.02f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f, 1.01f,
};
/// <summary>The number of steps required to get a circle with a maximum error of maxError</summary>
public static int CircleSteps (Matrix4x4 matrix, float radius, float maxError) {
// Take the maximum scale factor among the 3 axes.
// If the current matrix has a uniform scale then they are all the same.
var maxScaleFactor = math.sqrt(math.max(math.max(math.lengthsq((Vector3)matrix.GetColumn(0)), math.lengthsq((Vector3)matrix.GetColumn(1))), math.lengthsq((Vector3)matrix.GetColumn(2))));
var realWorldRadius = radius * maxScaleFactor;
// This expression is the first taylor expansion term of the formula below.
// It is almost identical to the formula below, but it avoids expensive trigonometric functions.
// return math.max(3, (int)math.ceil(math.PI * math.sqrt(realWorldRadius / (2*maxError))));
var cosAngle = 1 - maxError / realWorldRadius;
int steps = math.max(3, (int)math.ceil(math.PI / math.acos(cosAngle)));
return steps;
}
/// <summary>
/// Radius factor to adjust for circle approximation errors.
/// If a circle is approximated by fewer segments, it will be slightly smaller than the original circle.
/// This factor is used to adjust the radius of the circle so that the resulting circle will have roughly the same area as the original circle.
/// </summary>
#if MODULE_COLLECTIONS_2_0_0_OR_NEWER && UNITY_2022_2_OR_NEWER
[GenerateTestsForBurstCompatibility]
#endif
public static float CircleRadiusAdjustmentFactor (int steps) {
var index = steps - 3;
if (index < circleRadiusAdjustmentFactors.Length) {
if (index < 0) throw new System.ArgumentOutOfRangeException("Steps must be at least 3");
return circleRadiusAdjustmentFactors[index];
} else {
// Larger steps are approximately one
return 1;
}
}
}
[BurstCompile]
internal static class ColliderMeshBuilder2D {
static int GetShapes (Collider2D coll, PhysicsShapeGroup2D group, HashSet<Rigidbody2D> handledRigidbodies) {
#if !UNITY_6000_0_OR_NEWER
var rigid = coll.attachedRigidbody;
if (rigid != null) {
if (handledRigidbodies.Add(rigid)) {
// Trying to get the shapes from a collider that is attached to a rigidbody will log annoying errors (this seems like a Unity bug tbh),
// so we must call GetShapes on the rigidbody instead.
// Not quite sure which version of Unity stopped logging these errors, but they don't seem to be present in Unity 6 anymore.
return rigid.GetShapes(group);
} else {
return 0;
}
}
#endif
if (coll is TilemapCollider2D tilemapColl) {
// Ensure the tilemap is up to date
tilemapColl.ProcessTilemapChanges();
}
return coll.GetShapes(group);
}
public static int GenerateMeshesFromColliders (Collider2D[] colliders, int numColliders, float maxError, out NativeArray<float3> outputVertices, out NativeArray<int> outputIndices, out NativeArray<ShapeMesh> outputShapeMeshes) {
var group = new PhysicsShapeGroup2D();
var shapeList = new NativeList<PhysicsShape2D>(numColliders, Allocator.Temp);
var verticesList = new NativeList<Vector2>(numColliders*4, Allocator.Temp);
var matricesList = new NativeList<Matrix4x4>(numColliders, Allocator.Temp);
var colliderIndexList = new NativeList<int>(numColliders, Allocator.Temp);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
var tempHandle = AtomicSafetyHandle.GetTempMemoryHandle();
#endif
#if UNITY_6000_0_OR_NEWER
HashSet<Rigidbody2D> handledRigidbodies = null;
#else
var handledRigidbodies = new HashSet<Rigidbody2D>();
#endif
Profiler.BeginSample("GetShapes");
// Get the low level physics shapes from all colliders
var indexOffset = 0;
for (int i = 0; i < numColliders; i++) {
var coll = colliders[i];
// Prevent errors from being logged when calling GetShapes on a collider that has no shapes
if (coll == null || coll.shapeCount == 0) continue;
var shapeCount = GetShapes(coll, group, handledRigidbodies);
if (shapeCount == 0) continue;
shapeList.Length += shapeCount;
verticesList.Length += group.vertexCount;
var subShapes = shapeList.AsArray().GetSubArray(shapeList.Length - shapeCount, shapeCount);
var subVertices = verticesList.AsArray().GetSubArray(verticesList.Length - group.vertexCount, group.vertexCount);
// Using AsArray and then GetSubArray will create an invalid safety handle due to unity limitations.
// We work around this by setting the safety handle to a temporary handle.
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref subShapes, tempHandle);
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref subVertices, tempHandle);
#endif
group.GetShapeData(subShapes, subVertices);
for (int j = 0; j < shapeCount; j++) {
var shape = subShapes[j];
shape.vertexStartIndex += indexOffset;
subShapes[j] = shape;
}
indexOffset += subVertices.Length;
matricesList.AddReplicate(group.localToWorldMatrix, shapeCount);
colliderIndexList.AddReplicate(i, shapeCount);
}
Profiler.EndSample();
Assert.AreEqual(shapeList.Length, matricesList.Length);
Profiler.BeginSample("GenerateMeshes");
var vertexBuffer = new NativeList<float3>(Allocator.Temp);
var indexBuffer = new NativeList<int3>(Allocator.Temp);
var shapeSpan = shapeList.AsUnsafeSpan();
var verticesSpan = verticesList.AsUnsafeSpan().Reinterpret<float2>();
var matricesSpan = matricesList.AsUnsafeSpan();
var indexSpan = colliderIndexList.AsUnsafeSpan();
outputShapeMeshes = new NativeArray<ShapeMesh>(shapeList.Length, Allocator.Persistent);
var outputShapeMeshesSpan = outputShapeMeshes.AsUnsafeSpan();
int outputMeshCount;
unsafe {
outputMeshCount = GenerateMeshesFromShapes(
ref shapeSpan,
ref verticesSpan,
ref matricesSpan,
ref indexSpan,
ref UnsafeUtility.AsRef<UnsafeList<float3> >(vertexBuffer.GetUnsafeList()),
ref UnsafeUtility.AsRef<UnsafeList<int3> >(indexBuffer.GetUnsafeList()),
ref outputShapeMeshesSpan,
maxError
);
}
Profiler.EndSample();
Profiler.BeginSample("Copy");
outputVertices = vertexBuffer.ToArray(Allocator.Persistent);
outputIndices = new NativeArray<int>(indexBuffer.AsArray().Reinterpret<int>(12), Allocator.Persistent);
Profiler.EndSample();
return outputMeshCount;
}
public struct ShapeMesh {
public Matrix4x4 matrix;
public Bounds bounds;
public int startIndex;
public int endIndex;
public int tag;
}
static void AddCapsuleMesh (float2 c1, float2 c2, ref Matrix4x4 shapeMatrix, float radius, float maxError, ref UnsafeList<float3> outputVertices, ref UnsafeList<int3> outputIndices, ref float3 mn, ref float3 mx) {
var steps = math.max(4, CircleGeometryUtilities.CircleSteps(shapeMatrix, radius, maxError));
// We are only generating a semicircle at a time, so reduce the number of steps
steps = (steps / 2) + 1;
radius = radius * CircleGeometryUtilities.CircleRadiusAdjustmentFactor(2*(steps-1));
var center1 = new Vector3(c1.x, c1.y, 0);
var center2 = new Vector3(c2.x, c2.y, 0);
var axis = math.normalizesafe(c2 - c1);
var crossAxis = new float2(-axis.y, axis.x);
var dx = radius * new Vector3(crossAxis.x, crossAxis.y, 0);
var dy = radius * new Vector3(axis.x, axis.y, 0);
var angle = math.PI / (steps-1);
var startVertex = outputVertices.Length;
var startVertex2 = startVertex + steps;
outputVertices.Length += steps*2;
for (int j = 0; j < steps; j++) {
math.sincos(angle * j, out var sin, out var cos);
// Generate first semi-circle
var p = center1 + cos * dx - sin * dy;
mn = math.min(mn, p);
mx = math.max(mx, p);
outputVertices[startVertex + j] = p;
// Generate second semi-circle
p = center2 - cos * dx + sin * dy;
mn = math.min(mn, p);
mx = math.max(mx, p);
outputVertices[startVertex2 + j] = p;
}
var startIndex = outputIndices.Length;
var startIndex2 = startIndex + steps-2;
outputIndices.Length += (steps-2)*2;
for (int j = 1; j < steps - 1; j++) {
// Triangle for first semi-circle
outputIndices[startIndex + j - 1] = new int3(startVertex, startVertex + j, startVertex + j + 1);
// Triangle for second semi-circle
outputIndices[startIndex2 + j - 1] = new int3(startVertex2, startVertex2 + j, startVertex2 + j + 1);
}
// Generate the connection between the two semi-circles
outputIndices.Add(new int3(startVertex, startVertex + steps - 1, startVertex2));
outputIndices.Add(new int3(startVertex, startVertex2, startVertex2 + steps - 1));
}
[BurstCompile]
public static int GenerateMeshesFromShapes (
ref UnsafeSpan<PhysicsShape2D> shapes,
ref UnsafeSpan<float2> vertices,
ref UnsafeSpan<Matrix4x4> shapeMatrices,
ref UnsafeSpan<int> groupIndices,
ref UnsafeList<float3> outputVertices,
ref UnsafeList<int3> outputIndices,
ref UnsafeSpan<ShapeMesh> outputShapeMeshes,
float maxError
) {
var groupStartIndex = 0;
var mn = new float3(float.MaxValue, float.MaxValue, float.MaxValue);
var mx = new float3(float.MinValue, float.MinValue, float.MinValue);
int outputMeshIndex = 0;
for (int i = 0; i < shapes.Length; i++) {
var shape = shapes[i];
var shapeVertices = vertices.Slice(shape.vertexStartIndex, shape.vertexCount);
var shapeMatrix = shapeMatrices[i];
switch (shape.shapeType) {
case PhysicsShapeType2D.Circle: {
var steps = CircleGeometryUtilities.CircleSteps(shapeMatrix, shape.radius, maxError);
var radius = shape.radius * CircleGeometryUtilities.CircleRadiusAdjustmentFactor(steps);
var center = new Vector3(shapeVertices[0].x, shapeVertices[0].y, 0);
var d1 = new Vector3(radius, 0, 0);
var d2 = new Vector3(0, radius, 0);
var angle = 2 * math.PI / steps;
var startVertex = outputVertices.Length;
for (int j = 0; j < steps; j++) {
math.sincos(angle * j, out var sin, out var cos);
var p = center + cos * d1 + sin * d2;
mn = math.min(mn, p);
mx = math.max(mx, p);
outputVertices.Add(p);
}
for (int j = 1; j < steps; j++) {
outputIndices.Add(new int3(startVertex, startVertex + j, startVertex + (j + 1) % steps));
}
break;
}
case PhysicsShapeType2D.Capsule: {
var c1 = shapeVertices[0];
var c2 = shapeVertices[1];
AddCapsuleMesh(c1, c2, ref shapeMatrix, shape.radius, maxError, ref outputVertices, ref outputIndices, ref mn, ref mx);
break;
}
case PhysicsShapeType2D.Polygon: {
var startVertex = outputVertices.Length;
outputVertices.Resize(startVertex + shape.vertexCount, NativeArrayOptions.UninitializedMemory);
for (int j = 0; j < shape.vertexCount; j++) {
var p = new Vector3(shapeVertices[j].x, shapeVertices[j].y, 0);
mn = math.min(mn, p);
mx = math.max(mx, p);
outputVertices[startVertex + j] = p;
}
outputIndices.SetCapacity(math.ceilpow2(outputIndices.Length + (shape.vertexCount - 2)));
for (int j = 1; j < shape.vertexCount - 1; j++) {
outputIndices.AddNoResize(new int3(startVertex, startVertex + j, startVertex + j + 1));
}
break;
}
case PhysicsShapeType2D.Edges: {
if (shape.radius > maxError) {
for (int j = 0; j < shape.vertexCount - 1; j++) {
AddCapsuleMesh(shapeVertices[j], shapeVertices[j+1], ref shapeMatrix, shape.radius, maxError, ref outputVertices, ref outputIndices, ref mn, ref mx);
}
} else {
var startVertex = outputVertices.Length;
outputVertices.Resize(startVertex + shape.vertexCount, NativeArrayOptions.UninitializedMemory);
for (int j = 0; j < shape.vertexCount; j++) {
var p = new Vector3(shapeVertices[j].x, shapeVertices[j].y, 0);
mn = math.min(mn, p);
mx = math.max(mx, p);
outputVertices[startVertex + j] = p;
}
outputIndices.SetCapacity(math.ceilpow2(outputIndices.Length + (shape.vertexCount - 1)));
for (int j = 0; j < shape.vertexCount - 1; j++) {
// An edge is represented by a degenerate triangle
outputIndices.AddNoResize(new int3(startVertex + j, startVertex + j + 1, startVertex + j + 1));
}
}
break;
}
default:
throw new System.Exception("Unexpected PhysicsShapeType2D");
}
// Merge shapes which are in the same group into a single ShapeMesh struct.
// This is done to reduce the per-shape overhead a bit.
// Don't do it too much, though, since that can cause filtering to not work too well.
// For example if a recast graph recalculates a single tile in a 2D scene, we don't want to include the whole collider for the
// TilemapCollider2D in the scene when doing rasterization, only the shapes around the tile that is recalculated.
// We will still process the whole TilemapCollider2D (no way around that), but we want to be able to exclude shapes shapes as quickly as possible
// based on their bounding boxes.
const int DesiredTrianglesPerGroup = 100;
if (i == shapes.Length - 1 || groupIndices[i] != groupIndices[i+1] || outputIndices.Length - groupStartIndex > DesiredTrianglesPerGroup) {
// Transform the bounding box to world space
// This is not the tightest bounding box, but it is good enough
var m = new ToWorldMatrix(new float3x3((float4x4)shapeMatrix));
var bounds = new Bounds((mn + mx)*0.5f, mx - mn);
bounds = m.ToWorld(bounds);
bounds.center += (Vector3)shapeMatrix.GetColumn(3);
outputShapeMeshes[outputMeshIndex++] = new ShapeMesh {
bounds = bounds,
matrix = shapeMatrix,
startIndex = groupStartIndex * 3,
endIndex = outputIndices.Length * 3,
tag = groupIndices[i]
};
mn = new float3(float.MaxValue, float.MaxValue, float.MaxValue);
mx = new float3(float.MinValue, float.MinValue, float.MinValue);
groupStartIndex = outputIndices.Length;
}
}
return outputMeshIndex;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 65d5806c4978b7e46b69297ca838f91c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7d87dc471eec3ae4dac67ee232391350
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,102 @@
using Pathfinding.Jobs;
using Pathfinding.Sync;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Profiling;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Builds nodes and tiles and prepares them for pathfinding.
///
/// Takes input from a <see cref="TileBuilder"/> job and outputs a <see cref="BuildNodeTilesOutput"/>.
///
/// This job takes the following steps:
/// - Calculate connections between nodes inside each tile
/// - Create node and tile objects
/// - Connect adjacent tiles together
/// </summary>
public struct JobBuildNodes {
uint graphIndex;
public uint initialPenalty;
public bool recalculateNormals;
public float maxTileConnectionEdgeDistance;
Matrix4x4 graphToWorldSpace;
TileLayout tileLayout;
public class BuildNodeTilesOutput : IProgress, System.IDisposable {
public TileBuilder.TileBuilderOutput progressSource;
public NavmeshTile[] tiles;
public float Progress => progressSource.Progress;
public void Dispose () {
}
}
internal JobBuildNodes(RecastGraph graph, TileLayout tileLayout) {
this.tileLayout = tileLayout;
this.graphIndex = graph.graphIndex;
this.initialPenalty = graph.initialPenalty;
this.recalculateNormals = graph.RecalculateNormals;
this.maxTileConnectionEdgeDistance = graph.MaxTileConnectionEdgeDistance;
this.graphToWorldSpace = tileLayout.transform.matrix;
}
public Promise<BuildNodeTilesOutput> Schedule (DisposeArena arena, Promise<TileBuilder.TileBuilderOutput> preCutDependency, Promise<TileCutter.TileCutterOutput> postCutDependency) {
var postCutInput = postCutDependency.GetValue();
var preCutInput = preCutDependency.GetValue();
var tileRect = preCutInput.tileMeshes.tileRect;
NativeArray<TileMesh.TileMeshUnsafe> finalTileMeshes;
if (postCutInput.tileMeshes.tileMeshes.IsCreated) {
UnityEngine.Assertions.Assert.AreEqual(postCutInput.tileMeshes.tileMeshes.Length, tileRect.Area);
finalTileMeshes = postCutInput.tileMeshes.tileMeshes;
} else {
finalTileMeshes = preCutInput.tileMeshes.tileMeshes;
}
UnityEngine.Assertions.Assert.AreEqual(preCutInput.tileMeshes.tileMeshes.Length, tileRect.Area);
var tiles = new NavmeshTile[tileRect.Area];
var tilesGCHandle = System.Runtime.InteropServices.GCHandle.Alloc(tiles);
var nodeConnections = new NativeArray<JobCalculateTriangleConnections.TileNodeConnectionsUnsafe>(tileRect.Area, Allocator.Persistent);
var calculateConnectionsJob = new JobCalculateTriangleConnections {
tileMeshes = finalTileMeshes,
nodeConnections = nodeConnections,
}.Schedule(postCutDependency.handle);
var tileWorldSize = new Vector2(tileLayout.TileWorldSizeX, tileLayout.TileWorldSizeZ);
var createTilesJob = new JobCreateTiles {
// If any cutting is done, we need to save the pre-cut data to be able to re-cut tiles later
preCutTileMeshes = postCutInput.tileMeshes.tileMeshes.IsCreated ? preCutInput.tileMeshes.tileMeshes : default,
tileMeshes = finalTileMeshes,
tiles = tilesGCHandle,
tileRect = tileRect,
graphTileCount = tileLayout.tileCount,
graphIndex = graphIndex,
initialPenalty = initialPenalty,
recalculateNormals = recalculateNormals,
graphToWorldSpace = this.graphToWorldSpace,
tileWorldSize = tileWorldSize,
}.Schedule(postCutDependency.handle);
var applyConnectionsJob = new JobWriteNodeConnections {
nodeConnections = nodeConnections,
tiles = tilesGCHandle,
}.Schedule(JobHandle.CombineDependencies(calculateConnectionsJob, createTilesJob));
Profiler.BeginSample("Scheduling ConnectTiles");
var connectTilesDependency = JobConnectTiles.ScheduleBatch(tilesGCHandle, applyConnectionsJob, tileRect, tileWorldSize, maxTileConnectionEdgeDistance);
Profiler.EndSample();
arena.Add(tilesGCHandle);
arena.Add(nodeConnections);
return new Promise<BuildNodeTilesOutput>(connectTilesDependency, new BuildNodeTilesOutput {
progressSource = preCutInput,
tiles = tiles,
});
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd51eca97d285874d997d22edd420a27
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
using Pathfinding.Util;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using Pathfinding.Collections;
using Pathfinding.Sync;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Builds tiles from raw mesh vertices and indices.
///
/// This job takes the following steps:
/// - Transform all vertices using the <see cref="meshToGraph"/> matrix.
/// - Remove duplicate vertices
/// - If <see cref="recalculateNormals"/> is enabled: ensure all triangles are laid out in the clockwise direction.
/// </summary>
[BurstCompile(FloatMode = FloatMode.Default)]
public struct JobBuildTileMeshFromVertices : IJob {
public NativeArray<Vector3> vertices;
public NativeArray<int> indices;
public Matrix4x4 meshToGraph;
public NativeArray<TileMesh.TileMeshUnsafe> outputBuffers;
public bool recalculateNormals;
[BurstCompile(FloatMode = FloatMode.Fast)]
public struct JobTransformTileCoordinates : IJob {
public NativeArray<Vector3> vertices;
public NativeArray<Int3> outputVertices;
public Matrix4x4 matrix;
public void Execute () {
if (vertices.Length != outputVertices.Length) throw new System.ArgumentException("Input and output arrays must have the same length");
for (int i = 0; i < vertices.Length; i++) {
outputVertices[i] = (Int3)matrix.MultiplyPoint3x4(vertices[i]);
}
}
}
public static Promise<TileBuilder.TileBuilderOutput> Schedule (NativeArray<Vector3> vertices, NativeArray<int> indices, Matrix4x4 meshToGraph, bool recalculateNormals) {
if (vertices.Length > NavmeshBase.VertexIndexMask) throw new System.ArgumentException("Too many vertices in the navmesh graph. Provided " + vertices.Length + ", but the maximum number of vertices per tile is " + NavmeshBase.VertexIndexMask + ". You can raise this limit by enabling ASTAR_RECAST_LARGER_TILES in the A* Inspector Optimizations tab");
var outputBuffers = new NativeArray<TileMesh.TileMeshUnsafe>(1, Allocator.Persistent);
var job = new JobBuildTileMeshFromVertices {
vertices = vertices,
indices = indices,
meshToGraph = meshToGraph,
outputBuffers = outputBuffers,
recalculateNormals = recalculateNormals,
}.Schedule();
return new Promise<TileBuilder.TileBuilderOutput>(job, new TileBuilder.TileBuilderOutput {
// TODO: Tile world size is wrong
tileMeshes = new TileMeshesUnsafe(outputBuffers, new IntRect(0, 0, 0, 0), new Vector2(100000, 100000)),
});
}
public void Execute () {
var int3vertices = new NativeList<Int3>(vertices.Length, Allocator.Temp);
int3vertices.Length = vertices.Length;
var tags = new NativeList<int>(indices.Length / 3, Allocator.Temp);
tags.Length = indices.Length / 3;
var triangles = new NativeList<int>(indices.Length, Allocator.Temp);
triangles.AddRange(indices);
new JobTransformTileCoordinates {
vertices = vertices,
outputVertices = int3vertices.AsArray(),
matrix = meshToGraph,
}.Execute();
unsafe {
UnityEngine.Assertions.Assert.IsTrue(this.outputBuffers.Length == 1);
var tile = (TileMesh.TileMeshUnsafe*) this.outputBuffers.GetUnsafePtr();
new MeshUtility.JobRemoveDuplicateVertices {
vertices = int3vertices,
triangles = triangles,
tags = tags,
}.Execute();
// Convert the buffers to spans that own their memory.
// The spans may be smaller than the underlaying allocation,
// but the whole allocation will be freed using the span's Free method.
tile->verticesInTileSpace = int3vertices.AsUnsafeSpan<Int3>().Clone(Allocator.Persistent);
tile->triangles = triangles.AsUnsafeSpan<int>().Clone(Allocator.Persistent);
tile->tags = tags.AsUnsafeSpan().Reinterpret<uint>().Clone(Allocator.Persistent);
if (recalculateNormals) {
MeshUtility.MakeTrianglesClockwise(ref tile->verticesInTileSpace, ref tile->triangles);
}
}
int3vertices.Dispose();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a22b53fa064d9344988e2a86b73851b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,288 @@
using Pathfinding.Jobs;
using Pathfinding.Util;
using Pathfinding.Graphs.Navmesh.Voxelization.Burst;
using Pathfinding.Collections;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using Unity.Profiling;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Scratch space for building navmesh tiles using voxelization.
///
/// This uses quite a lot of memory, so it is used by a single worker thread for multiple tiles in order to minimize allocations.
/// </summary>
public struct TileBuilderBurst : IArenaDisposable {
public LinkedVoxelField linkedVoxelField;
public CompactVoxelField compactVoxelField;
public NativeList<ushort> distanceField;
public NativeQueue<Int3> tmpQueue1;
public NativeQueue<Int3> tmpQueue2;
public NativeList<VoxelContour> contours;
public NativeList<int> contourVertices;
public VoxelMesh voxelMesh;
public TileBuilderBurst (int width, int depth, int voxelWalkableHeight, int maximumVoxelYCoord) {
linkedVoxelField = new LinkedVoxelField(width, depth, maximumVoxelYCoord);
compactVoxelField = new CompactVoxelField(width, depth, voxelWalkableHeight, Allocator.Persistent);
tmpQueue1 = new NativeQueue<Int3>(Allocator.Persistent);
tmpQueue2 = new NativeQueue<Int3>(Allocator.Persistent);
distanceField = new NativeList<ushort>(0, Allocator.Persistent);
contours = new NativeList<VoxelContour>(Allocator.Persistent);
contourVertices = new NativeList<int>(Allocator.Persistent);
voxelMesh = new VoxelMesh {
verts = new NativeList<Int3>(Allocator.Persistent),
tris = new NativeList<int>(Allocator.Persistent),
areas = new NativeList<int>(Allocator.Persistent),
};
}
void IArenaDisposable.DisposeWith (DisposeArena arena) {
arena.Add(linkedVoxelField);
arena.Add(compactVoxelField);
arena.Add(distanceField);
arena.Add(tmpQueue1);
arena.Add(tmpQueue2);
arena.Add(contours);
arena.Add(contourVertices);
arena.Add(voxelMesh);
}
}
/// <summary>
/// Builds tiles from a polygon soup using voxelization.
///
/// This job takes the following steps:
/// - Voxelize the input meshes
/// - Filter and process the resulting voxelization in various ways to remove unwanted artifacts and make it better suited for pathfinding.
/// - Extract a walkable surface from the voxelization.
/// - Triangulate this surface and create navmesh tiles from it.
///
/// This job uses work stealing to distribute the work between threads. The communication happens using a shared queue and the <see cref="currentTileCounter"/> atomic variable.
/// </summary>
[BurstCompile(CompileSynchronously = true)]
// TODO: [BurstCompile(FloatMode = FloatMode.Fast)]
public struct JobBuildTileMeshFromVoxels : IJob {
public TileBuilderBurst tileBuilder;
[ReadOnly]
public TileBuilder.BucketMapping inputMeshes;
[ReadOnly]
public NativeArray<Bounds> tileGraphSpaceBounds;
public Matrix4x4 voxelToTileSpace;
/// <summary>
/// Limits of the graph space bounds for the whole graph on the XZ plane.
///
/// Used to crop the border tiles to exactly the limits of the graph's bounding box.
/// </summary>
public Vector2 graphSpaceLimits;
[NativeDisableUnsafePtrRestriction]
public unsafe TileMesh.TileMeshUnsafe* outputMeshes;
/// <summary>Max number of tiles to process in this job</summary>
public int maxTiles;
public int voxelWalkableClimb;
public uint voxelWalkableHeight;
public float cellSize;
public float cellHeight;
public float maxSlope;
public RecastGraph.DimensionMode dimensionMode;
public RecastGraph.BackgroundTraversability backgroundTraversability;
public Matrix4x4 graphToWorldSpace;
public int characterRadiusInVoxels;
public int tileBorderSizeInVoxels;
public int minRegionSize;
public float maxEdgeLength;
public float contourMaxError;
[ReadOnly]
public NativeArray<JobBuildRegions.RelevantGraphSurfaceInfo> relevantGraphSurfaces;
public RecastGraph.RelevantGraphSurfaceMode relevantGraphSurfaceMode;
[NativeDisableUnsafePtrRestriction]
public unsafe int* currentTileCounter;
public void SetOutputMeshes (NativeArray<TileMesh.TileMeshUnsafe> arr) {
unsafe {
outputMeshes = (TileMesh.TileMeshUnsafe*)arr.GetUnsafeReadOnlyPtr();
}
}
public void SetCounter (NativeReference<int> counter) {
unsafe {
// Note: The pointer cast is only necessary when using early versions of the collections package.
currentTileCounter = (int*)counter.GetUnsafePtr();
}
}
private static readonly ProfilerMarker MarkerVoxelize = new ProfilerMarker("Voxelize");
private static readonly ProfilerMarker MarkerFilterLedges = new ProfilerMarker("FilterLedges");
private static readonly ProfilerMarker MarkerFilterLowHeightSpans = new ProfilerMarker("FilterLowHeightSpans");
private static readonly ProfilerMarker MarkerBuildCompactField = new ProfilerMarker("BuildCompactField");
private static readonly ProfilerMarker MarkerBuildConnections = new ProfilerMarker("BuildConnections");
private static readonly ProfilerMarker MarkerErodeWalkableArea = new ProfilerMarker("ErodeWalkableArea");
private static readonly ProfilerMarker MarkerBuildDistanceField = new ProfilerMarker("BuildDistanceField");
private static readonly ProfilerMarker MarkerBuildRegions = new ProfilerMarker("BuildRegions");
private static readonly ProfilerMarker MarkerBuildContours = new ProfilerMarker("BuildContours");
private static readonly ProfilerMarker MarkerBuildMesh = new ProfilerMarker("BuildMesh");
private static readonly ProfilerMarker MarkerConvertAreasToTags = new ProfilerMarker("ConvertAreasToTags");
private static readonly ProfilerMarker MarkerRemoveDuplicateVertices = new ProfilerMarker("RemoveDuplicateVertices");
private static readonly ProfilerMarker MarkerTransformTileCoordinates = new ProfilerMarker("TransformTileCoordinates");
public void Execute () {
for (int k = 0; k < maxTiles; k++) {
// Grab the next tile index that we should calculate
int i;
unsafe {
i = System.Threading.Interlocked.Increment(ref UnsafeUtility.AsRef<int>(currentTileCounter)) - 1;
}
if (i >= tileGraphSpaceBounds.Length) return;
tileBuilder.linkedVoxelField.ResetLinkedVoxelSpans();
if (dimensionMode == RecastGraph.DimensionMode.Dimension2D && backgroundTraversability == RecastGraph.BackgroundTraversability.Walkable) {
tileBuilder.linkedVoxelField.SetWalkableBackground();
}
var bucketStart = i > 0 ? inputMeshes.bucketRanges[i-1] : 0;
var bucketEnd = inputMeshes.bucketRanges[i];
MarkerVoxelize.Begin();
new JobVoxelize {
inputMeshes = inputMeshes.meshes,
bucket = inputMeshes.pointers.GetSubArray(bucketStart, bucketEnd - bucketStart),
voxelWalkableClimb = voxelWalkableClimb,
voxelWalkableHeight = voxelWalkableHeight,
cellSize = cellSize,
cellHeight = cellHeight,
maxSlope = maxSlope,
graphTransform = graphToWorldSpace,
graphSpaceBounds = tileGraphSpaceBounds[i],
graphSpaceLimits = graphSpaceLimits,
voxelArea = tileBuilder.linkedVoxelField,
}.Execute();
MarkerVoxelize.End();
MarkerFilterLedges.Begin();
new JobFilterLedges {
field = tileBuilder.linkedVoxelField,
voxelWalkableClimb = voxelWalkableClimb,
voxelWalkableHeight = voxelWalkableHeight,
cellSize = cellSize,
cellHeight = cellHeight,
}.Execute();
MarkerFilterLedges.End();
MarkerFilterLowHeightSpans.Begin();
new JobFilterLowHeightSpans {
field = tileBuilder.linkedVoxelField,
voxelWalkableHeight = voxelWalkableHeight,
}.Execute();
MarkerFilterLowHeightSpans.End();
MarkerBuildCompactField.Begin();
new JobBuildCompactField {
input = tileBuilder.linkedVoxelField,
output = tileBuilder.compactVoxelField,
}.Execute();
MarkerBuildCompactField.End();
MarkerBuildConnections.Begin();
new JobBuildConnections {
field = tileBuilder.compactVoxelField,
voxelWalkableHeight = (int)voxelWalkableHeight,
voxelWalkableClimb = voxelWalkableClimb,
}.Execute();
MarkerBuildConnections.End();
MarkerErodeWalkableArea.Begin();
new JobErodeWalkableArea {
field = tileBuilder.compactVoxelField,
radius = characterRadiusInVoxels,
}.Execute();
MarkerErodeWalkableArea.End();
MarkerBuildDistanceField.Begin();
new JobBuildDistanceField {
field = tileBuilder.compactVoxelField,
output = tileBuilder.distanceField,
}.Execute();
MarkerBuildDistanceField.End();
MarkerBuildRegions.Begin();
new JobBuildRegions {
field = tileBuilder.compactVoxelField,
distanceField = tileBuilder.distanceField,
borderSize = tileBorderSizeInVoxels,
minRegionSize = Mathf.RoundToInt(minRegionSize),
srcQue = tileBuilder.tmpQueue1,
dstQue = tileBuilder.tmpQueue2,
relevantGraphSurfaces = relevantGraphSurfaces,
relevantGraphSurfaceMode = relevantGraphSurfaceMode,
cellSize = cellSize,
cellHeight = cellHeight,
graphTransform = graphToWorldSpace,
graphSpaceBounds = tileGraphSpaceBounds[i],
}.Execute();
MarkerBuildRegions.End();
MarkerBuildContours.Begin();
new JobBuildContours {
field = tileBuilder.compactVoxelField,
maxError = contourMaxError,
maxEdgeLength = maxEdgeLength,
buildFlags = VoxelUtilityBurst.RC_CONTOUR_TESS_WALL_EDGES | VoxelUtilityBurst.RC_CONTOUR_TESS_TILE_EDGES,
cellSize = cellSize,
outputContours = tileBuilder.contours,
outputVerts = tileBuilder.contourVertices,
}.Execute();
MarkerBuildContours.End();
MarkerBuildMesh.Begin();
new JobBuildMesh {
contours = tileBuilder.contours,
contourVertices = tileBuilder.contourVertices,
mesh = tileBuilder.voxelMesh,
field = tileBuilder.compactVoxelField,
}.Execute();
MarkerBuildMesh.End();
unsafe {
TileMesh.TileMeshUnsafe* outputTileMesh = outputMeshes + i;
MarkerConvertAreasToTags.Begin();
new JobConvertAreasToTags {
areas = tileBuilder.voxelMesh.areas,
}.Execute();
MarkerConvertAreasToTags.End();
MarkerRemoveDuplicateVertices.Begin();
new MeshUtility.JobRemoveDuplicateVertices {
vertices = tileBuilder.voxelMesh.verts,
triangles = tileBuilder.voxelMesh.tris,
tags = tileBuilder.voxelMesh.areas,
}.Execute();
MarkerRemoveDuplicateVertices.End();
MarkerTransformTileCoordinates.Begin();
new JobTransformTileCoordinates {
vertices = tileBuilder.voxelMesh.verts.AsUnsafeSpan(),
matrix = voxelToTileSpace,
}.Execute();
MarkerTransformTileCoordinates.End();
*outputTileMesh = new TileMesh.TileMeshUnsafe {
// Convert the buffers to spans that own their memory.
verticesInTileSpace = tileBuilder.voxelMesh.verts.AsUnsafeSpan().Clone(Allocator.Persistent),
triangles = tileBuilder.voxelMesh.tris.AsUnsafeSpan().Clone(Allocator.Persistent),
tags = tileBuilder.voxelMesh.areas.AsUnsafeSpan().Reinterpret<uint>().Clone(Allocator.Persistent),
};
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20aeb827260a74a4492e7687fdebb14f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,74 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine.Assertions;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Calculates node connections between triangles within each tile.
/// Connections between tiles are handled at a later stage in <see cref="JobConnectTiles"/>.
/// </summary>
[BurstCompile]
public struct JobCalculateTriangleConnections : IJob {
[ReadOnly]
public NativeArray<TileMesh.TileMeshUnsafe> tileMeshes;
[WriteOnly]
public NativeArray<TileNodeConnectionsUnsafe> nodeConnections;
public struct TileNodeConnectionsUnsafe {
/// <summary>Stream of packed connection edge infos (from <see cref="Connection.PackShapeEdgeInfo"/>)</summary>
public Unity.Collections.LowLevel.Unsafe.UnsafeAppendBuffer neighbours;
/// <summary>Number of neighbours for each triangle</summary>
public Unity.Collections.LowLevel.Unsafe.UnsafeAppendBuffer neighbourCounts;
}
public void Execute () {
Assert.AreEqual(tileMeshes.Length, nodeConnections.Length);
var nodeRefs = new NativeParallelHashMap<int2, uint>(128, Allocator.Temp);
bool duplicates = false;
for (int ti = 0; ti < tileMeshes.Length; ti++) {
nodeRefs.Clear();
var tile = tileMeshes[ti];
var numIndices = tile.triangles.Length;
var neighbours = new Unity.Collections.LowLevel.Unsafe.UnsafeAppendBuffer(numIndices * 2 * 4, 4, Allocator.Persistent);
var neighbourCounts = new Unity.Collections.LowLevel.Unsafe.UnsafeAppendBuffer(numIndices * 4, 4, Allocator.Persistent);
const int TriangleIndexBits = 28;
unsafe {
Assert.IsTrue(numIndices % 3 == 0);
// Access data via the raw pointer to avoid bounds checks
var triangles = tile.triangles.ptr;
for (int i = 0, j = 0; i < numIndices; i += 3, j++) {
duplicates |= !nodeRefs.TryAdd(new int2(triangles[i+0], triangles[i+1]), (uint)j | (0 << TriangleIndexBits));
duplicates |= !nodeRefs.TryAdd(new int2(triangles[i+1], triangles[i+2]), (uint)j | (1 << TriangleIndexBits));
duplicates |= !nodeRefs.TryAdd(new int2(triangles[i+2], triangles[i+0]), (uint)j | (2 << TriangleIndexBits));
}
for (int i = 0; i < numIndices; i += 3) {
var cnt = 0;
for (int edge = 0; edge < 3; edge++) {
if (nodeRefs.TryGetValue(new int2(triangles[i+((edge+1) % 3)], triangles[i+edge]), out var match)) {
var other = match & ((1 << TriangleIndexBits) - 1);
var otherEdge = (int)(match >> TriangleIndexBits);
neighbours.Add(other);
var edgeInfo = Connection.PackShapeEdgeInfo((byte)edge, (byte)otherEdge, true, true, true);
neighbours.Add((int)edgeInfo);
cnt += 1;
}
}
neighbourCounts.Add(cnt);
}
}
nodeConnections[ti] = new TileNodeConnectionsUnsafe {
neighbours = neighbours,
neighbourCounts = neighbourCounts,
};
}
if (duplicates) {
UnityEngine.Debug.LogWarning("Duplicate triangle edges were found in the input mesh. These have been removed. Are you sure your mesh is suitable for being used as a navmesh directly?\nThis could be caused by the mesh's normals not being consistent.");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 30417132dbc15504abbdf1b70224c006
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,159 @@
using Unity.Collections;
using Unity.Jobs;
using Unity.Jobs.LowLevel.Unsafe;
using Unity.Mathematics;
using UnityEngine;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Connects adjacent tiles together.
///
/// This only creates connections between tiles. Connections internal to a tile should be handled by <see cref="JobCalculateTriangleConnections"/>.
///
/// Use the <see cref="ScheduleBatch"/> method to connect a bunch of tiles efficiently using maximum parallelism.
/// </summary>
public struct JobConnectTiles : IJob {
/// <summary>GCHandle referring to a NavmeshTile[] array of size tileRect.Width*tileRect.Height</summary>
public System.Runtime.InteropServices.GCHandle tiles;
public int coordinateSum;
public int direction;
public int zOffset;
public int zStride;
Vector2 tileWorldSize;
IntRect tileRect;
/// <summary>Maximum vertical distance between two tiles to create a connection between them</summary>
public float maxTileConnectionEdgeDistance;
static readonly Unity.Profiling.ProfilerMarker ConnectTilesMarker = new Unity.Profiling.ProfilerMarker("ConnectTiles");
/// <summary>
/// Schedule jobs to connect all the given tiles with each other while exploiting as much parallelism as possible.
/// tilesHandle should be a GCHandle referring to a NavmeshTile[] array of size tileRect.Width*tileRect.Height.
/// </summary>
public static JobHandle ScheduleBatch (System.Runtime.InteropServices.GCHandle tilesHandle, JobHandle dependency, IntRect tileRect, Vector2 tileWorldSize, float maxTileConnectionEdgeDistance) {
// First connect all tiles with an EVEN coordinate sum
// This would be the white squares on a chess board.
// Then connect all tiles with an ODD coordinate sum (which would be all black squares on a chess board).
// This will prevent the different threads that do all
// this in parallel from conflicting with each other.
// The directions are also done separately
// first they are connected along the X direction and then along the Z direction.
// Looping over 0 and then 1
int workers = Mathf.Max(1, JobsUtility.JobWorkerCount);
var handles = new NativeArray<JobHandle>(workers, Allocator.Temp);
for (int coordinateSum = 0; coordinateSum <= 1; coordinateSum++) {
for (int direction = 0; direction <= 1; direction++) {
for (int i = 0; i < workers; i++) {
handles[i] = new JobConnectTiles {
tiles = tilesHandle,
tileRect = tileRect,
tileWorldSize = tileWorldSize,
coordinateSum = coordinateSum,
direction = direction,
maxTileConnectionEdgeDistance = maxTileConnectionEdgeDistance,
zOffset = i,
zStride = workers,
}.Schedule(dependency);
}
dependency = JobHandle.CombineDependencies(handles);
}
}
return dependency;
}
/// <summary>
/// Schedule jobs to connect all the given tiles inside innerRect with tiles that are outside it, while exploiting as much parallelism as possible.
/// tilesHandle should be a GCHandle referring to a NavmeshTile[] array of size tileRect.Width*tileRect.Height.
/// </summary>
public static JobHandle ScheduleRecalculateBorders (System.Runtime.InteropServices.GCHandle tilesHandle, JobHandle dependency, IntRect tileRect, IntRect innerRect, Vector2 tileWorldSize, float maxTileConnectionEdgeDistance) {
var w = innerRect.Width;
var h = innerRect.Height;
// Note: conservative estimate of number of handles. There may be fewer in reality.
var allDependencies = new NativeArray<JobHandle>(2*w + 2*math.max(0, h - 2), Allocator.Temp);
int count = 0;
for (int z = 0; z < h; z++) {
for (int x = 0; x < w; x++) {
// Check if the tile is on the border of the inner rect
if (!(x == 0 || z == 0 || x == w - 1 || z == h - 1)) continue;
var tileX = innerRect.xmin + x;
var tileZ = innerRect.ymin + z;
// For a corner tile, the jobs need to run sequentially
var dep = dependency;
for (int direction = 0; direction < 4; direction++) {
var nx = tileX + (direction == 0 ? 1 : direction == 1 ? -1 : 0);
var nz = tileZ + (direction == 2 ? 1 : direction == 3 ? -1 : 0);
if (innerRect.Contains(nx, nz) || !tileRect.Contains(nx, nz)) {
continue;
}
dep = new JobConnectTilesSingle {
tiles = tilesHandle,
tileIndex1 = tileX + tileZ * tileRect.Width,
tileIndex2 = nx + nz * tileRect.Width,
tileWorldSize = tileWorldSize,
maxTileConnectionEdgeDistance = maxTileConnectionEdgeDistance,
}.Schedule(dep);
}
allDependencies[count++] = dep;
}
}
return JobHandle.CombineDependencies(allDependencies);
}
public void Execute () {
var tiles = (NavmeshTile[])this.tiles.Target;
var tileRectDepth = tileRect.Height;
var tileRectWidth = tileRect.Width;
for (int z = zOffset; z < tileRectDepth; z += zStride) {
for (int x = 0; x < tileRectWidth; x++) {
if ((x + z) % 2 == coordinateSum) {
int tileIndex1 = x + z * tileRectWidth;
int tileIndex2;
if (direction == 0 && x < tileRectWidth - 1) {
tileIndex2 = x + 1 + z * tileRectWidth;
} else if (direction == 1 && z < tileRectDepth - 1) {
tileIndex2 = x + (z + 1) * tileRectWidth;
} else {
continue;
}
ConnectTilesMarker.Begin();
NavmeshBase.ConnectTiles(tiles[tileIndex1], tiles[tileIndex2], tileWorldSize.x, tileWorldSize.y, maxTileConnectionEdgeDistance);
ConnectTilesMarker.End();
}
}
}
}
}
/// <summary>
/// Connects two adjacent tiles together.
///
/// This only creates connections between tiles. Connections internal to a tile should be handled by <see cref="JobCalculateTriangleConnections"/>.
/// </summary>
struct JobConnectTilesSingle : IJob {
/// <summary>GCHandle referring to a NavmeshTile[] array of size tileRect.Width*tileRect.Height</summary>
public System.Runtime.InteropServices.GCHandle tiles;
/// <summary>Index of the first tile in the <see cref="tiles"/> array</summary>
public int tileIndex1;
/// <summary>Index of the second tile in the <see cref="tiles"/> array</summary>
public int tileIndex2;
/// <summary>Size of a tile in world units</summary>
public Vector2 tileWorldSize;
/// <summary>Maximum vertical distance between two tiles to create a connection between them</summary>
public float maxTileConnectionEdgeDistance;
public void Execute () {
var tiles = (NavmeshTile[])this.tiles.Target;
NavmeshBase.ConnectTiles(tiles[tileIndex1], tiles[tileIndex2], tileWorldSize.x, tileWorldSize.y, maxTileConnectionEdgeDistance);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd00a18824d04764783722c547fb60f7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using Pathfinding.Graphs.Navmesh.Voxelization.Burst;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>Convert recast region IDs to the tags that should be applied to the nodes</summary>
[BurstCompile]
public struct JobConvertAreasToTags : IJob {
public NativeList<int> areas;
public void Execute () {
unsafe {
for (int i = 0; i < areas.Length; i++) {
var area = areas[i];
// The user supplied IDs start at 1 because 0 is reserved for NotWalkable
areas[i] = (area & VoxelUtilityBurst.TagReg) != 0 ? (area & VoxelUtilityBurst.TagRegMask) - 1 : 0;
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 229fdb01207c1ab4796deea78744e136
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,140 @@
using Pathfinding.Collections;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Profiling;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Builds tiles optimized for pathfinding, from a list of <see cref="TileMesh.TileMeshUnsafe"/>.
///
/// This job takes the following steps:
/// - Transform all vertices using the <see cref="graphToWorldSpace"/> matrix.
/// - Remove duplicate vertices
/// - If <see cref="recalculateNormals"/> is enabled: ensure all triangles are laid out in the clockwise direction.
/// </summary>
public struct JobCreateTiles : IJob {
/// <summary>An array of <see cref="TileMesh.TileMeshUnsafe"/> of length tileRect.Width*tileRect.Height, or an uninitialized array</summary>
[ReadOnly]
[NativeDisableContainerSafetyRestriction]
public NativeArray<TileMesh.TileMeshUnsafe> preCutTileMeshes;
/// <summary>An array of <see cref="TileMesh.TileMeshUnsafe"/> of length tileRect.Width*tileRect.Height</summary>
[ReadOnly]
public NativeArray<TileMesh.TileMeshUnsafe> tileMeshes;
/// <summary>
/// An array of <see cref="NavmeshTile"/> of length tileRect.Width*tileRect.Height.
/// This array will be filled with the created tiles.
/// </summary>
public System.Runtime.InteropServices.GCHandle tiles;
/// <summary>Graph index of the graph that these nodes will be added to</summary>
public uint graphIndex;
/// <summary>
/// Number of tiles in the graph.
///
/// This may be much bigger than the <see cref="tileRect"/> that we are actually processing.
/// For example if a graph update is performed, the <see cref="tileRect"/> will just cover the tiles that are recalculated,
/// while <see cref="graphTileCount"/> will contain all tiles in the graph.
/// </summary>
public Vector2Int graphTileCount;
/// <summary>
/// Rectangle of tiles that we are processing.
///
/// (xmax, ymax) must be smaller than graphTileCount.
/// If for examples <see cref="graphTileCount"/> is (10, 10) and <see cref="tileRect"/> is {2, 3, 5, 6} then we are processing tiles (2, 3) to (5, 6) inclusive.
/// </summary>
public IntRect tileRect;
/// <summary>Initial penalty for all nodes in the tile</summary>
public uint initialPenalty;
/// <summary>
/// If true, all triangles will be guaranteed to be laid out in clockwise order.
/// If false, their original order will be preserved.
/// </summary>
public bool recalculateNormals;
/// <summary>Size of a tile in world units along the graph's X and Z axes</summary>
public Vector2 tileWorldSize;
/// <summary>Matrix to convert from graph space to world space</summary>
public Matrix4x4 graphToWorldSpace;
public void Execute () {
var tiles = (NavmeshTile[])this.tiles.Target;
Assert.AreEqual(tileMeshes.Length, tiles.Length);
Assert.AreEqual(tileMeshes.Length, tileRect.Area);
Assert.IsTrue(tileRect.xmax < graphTileCount.x);
Assert.IsTrue(tileRect.ymax < graphTileCount.y);
var tileRectWidth = tileRect.Width;
var tileRectDepth = tileRect.Height;
bool isUsingCuts = preCutTileMeshes.IsCreated;
if (isUsingCuts) {
Assert.AreEqual(preCutTileMeshes.Length, tiles.Length);
Assert.AreEqual(preCutTileMeshes.Length, tileRect.Area);
}
for (int z = 0; z < tileRectDepth; z++) {
for (int x = 0; x < tileRectWidth; x++) {
var tileIndex = z*tileRectWidth + x;
// If we are just updating a part of the graph we still want to assign the nodes the proper global tile index
var graphTileIndex = (z + tileRect.ymin)*graphTileCount.x + (x + tileRect.xmin);
var mesh = tileMeshes[tileIndex];
// Convert tile space to graph space and world space
var verticesInGraphSpace = mesh.verticesInTileSpace.Clone(Allocator.Persistent);
var verticesInWorldSpace = verticesInGraphSpace.Clone(Allocator.Persistent);
var tileSpaceToGraphSpaceOffset = (Int3) new Vector3(tileWorldSize.x * (x + tileRect.xmin), 0, tileWorldSize.y * (z + tileRect.ymin));
for (int i = 0; i < verticesInGraphSpace.Length; i++) {
var v = verticesInGraphSpace[i] + tileSpaceToGraphSpaceOffset;
verticesInGraphSpace[i] = v;
verticesInWorldSpace[i] = (Int3)graphToWorldSpace.MultiplyPoint3x4((Vector3)v);
}
// Create a new navmesh tile and assign its settings
var triangles = mesh.triangles.Clone(Allocator.Persistent);
var tile = new NavmeshTile {
x = x + tileRect.xmin,
z = z + tileRect.ymin,
w = 1,
d = 1,
tris = triangles,
vertsInGraphSpace = verticesInGraphSpace,
verts = verticesInWorldSpace,
bbTree = new BBTree(triangles, verticesInGraphSpace),
nodes = new TriangleMeshNode[triangles.Length/3],
// Leave empty for now, it will be filled in later
graph = null,
isCut = false,
};
if (isUsingCuts) {
// Copy pre-cut data to be able to reference it when re-cutting the tile.
// If no cuts are used, we don't save the pre-cut data, to reduce memory usage,
// as it is identical to the post-cut data.
// These arrays can be re-created from the other tile data, if needed.
var preCutMesh = preCutTileMeshes[tileIndex];
tile.preCutVertsInTileSpace = preCutMesh.verticesInTileSpace.Clone(Allocator.Persistent);
tile.preCutTris = preCutMesh.triangles.Clone(Allocator.Persistent);
tile.preCutTags = preCutMesh.tags.Clone(Allocator.Persistent);
tile.isCut = true;
}
Profiler.BeginSample("CreateNodes");
NavmeshBase.CreateNodes(tile, tile.tris, graphTileIndex, graphIndex, mesh.tags, false, null, initialPenalty, false);
Profiler.EndSample();
tiles[tileIndex] = tile;
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b86cf43938afd654a8f1b711e55977d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,30 @@
using Pathfinding.Util;
using Pathfinding.Collections;
using Unity.Burst;
using Unity.Jobs;
using UnityEngine;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Transforms vertices from voxel coordinates to tile coordinates.
///
/// This essentially constitutes multiplying the vertices by the <see cref="matrix"/>.
///
/// Note: The input space is in raw voxel coordinates, the output space is in tile coordinates stored in millimeters (as is typical for the Int3 struct. See <see cref="Int3.Precision"/>).
/// </summary>
[BurstCompile(FloatMode = FloatMode.Fast)]
public struct JobTransformTileCoordinates : IJob {
public unsafe UnsafeSpan<Int3> vertices;
public Matrix4x4 matrix;
public void Execute () {
unsafe {
for (uint i = 0; i < vertices.length; i++) {
// Transform from voxel indices to a proper Int3 coordinate, then convert it to a Vector3 float coordinate
var p = vertices[i];
vertices[i] = (Int3)matrix.MultiplyPoint3x4(new Vector3(p.x, p.y, p.z));
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ff97d8db3ca9a074dbfbd83fa5ad16be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
using Pathfinding.Pooling;
using Unity.Collections;
using Unity.Jobs;
using UnityEngine.Assertions;
using UnityEngine.Profiling;
namespace Pathfinding.Graphs.Navmesh.Jobs {
/// <summary>
/// Writes connections to each node in each tile.
///
/// It also calculates the connection costs between nodes.
///
/// This job is run after all tiles have been built and the connections have been calculated.
///
/// See: <see cref="JobCalculateTriangleConnections"/>
/// </summary>
public struct JobWriteNodeConnections : IJob {
/// <summary>Connections for each tile</summary>
[ReadOnly]
public NativeArray<JobCalculateTriangleConnections.TileNodeConnectionsUnsafe> nodeConnections;
/// <summary>Array of <see cref="NavmeshTile"/></summary>
public System.Runtime.InteropServices.GCHandle tiles;
public void Execute () {
var tiles = (NavmeshTile[])this.tiles.Target;
Assert.AreEqual(nodeConnections.Length, tiles.Length);
for (int i = 0; i < tiles.Length; i++) {
Profiler.BeginSample("CreateConnections");
var connections = nodeConnections[i];
Apply(tiles[i].nodes, connections);
connections.neighbourCounts.Dispose();
connections.neighbours.Dispose();
Profiler.EndSample();
}
}
void Apply (TriangleMeshNode[] nodes, JobCalculateTriangleConnections.TileNodeConnectionsUnsafe connections) {
var neighbourCountsReader = connections.neighbourCounts.AsReader();
var neighboursReader = connections.neighbours.AsReader();
for (int i = 0; i < nodes.Length; i++) {
var node = nodes[i];
var neighbourCount = neighbourCountsReader.ReadNext<int>();
var conns = node.connections = ArrayPool<Connection>.ClaimWithExactLength(neighbourCount);
for (int j = 0; j < neighbourCount; j++) {
var otherIndex = neighboursReader.ReadNext<int>();
var shapeEdgeInfo = (byte)neighboursReader.ReadNext<int>();
var other = nodes[otherIndex];
var cost = (node.position - other.position).costMagnitude;
conns[j] = new Connection(
other,
(uint)cost,
shapeEdgeInfo
);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eea3ec9fc5dd8604c9902e09277d86d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,129 @@
namespace Pathfinding.Graphs.Navmesh {
using Pathfinding.Util;
using Pathfinding.Collections;
using Unity.Collections;
/// <summary>
/// A single tile in a recast or navmesh graph.
///
/// A tile is a single rectangular (but usually square) part of the graph.
/// Tiles can be updated individually, which is great for large worlds where updating the whole graph would take a long time.
/// </summary>
public class NavmeshTile : INavmeshHolder {
/// <summary>
/// All vertices in the tile.
/// The vertices are in graph space.
///
/// This represents an allocation using the Persistent allocator.
/// </summary>
public UnsafeSpan<Int3> vertsInGraphSpace;
/// <summary>
/// All vertices in the tile.
/// The vertices are in world space.
///
/// This represents an allocation using the Persistent allocator.
/// </summary>
public UnsafeSpan<Int3> verts;
/// <summary>
/// All triangle indices in the tile.
/// One triangle is 3 indices.
/// The triangles are in the same order as the <see cref="nodes"/>.
///
/// This represents an allocation using the Persistent allocator.
/// </summary>
public UnsafeSpan<int> tris;
/// <summary>
/// True if this tile may have been cut by <see cref="NavmeshCut"/>s, or had pieces added by <see cref="NavmeshAdd"/> components.
///
/// If true, the <see cref="preCutVertsInTileSpace"/>, <see cref="preCutTris"/> and <see cref="preCutTags"/> fields will be valid.
/// </summary>
public bool isCut;
public UnsafeSpan<Int3> preCutVertsInTileSpace;
public UnsafeSpan<int> preCutTris;
public UnsafeSpan<uint> preCutTags;
/// <summary>Tile X Coordinate</summary>
public int x;
/// <summary>Tile Z Coordinate</summary>
public int z;
/// <summary>
/// Width, in tile coordinates.
/// Warning: Widths other than 1 are not supported. This is mainly here for possible future features.
/// </summary>
public int w;
/// <summary>
/// Depth, in tile coordinates.
/// Warning: Depths other than 1 are not supported. This is mainly here for possible future features.
/// </summary>
public int d;
/// <summary>All nodes in the tile</summary>
public TriangleMeshNode[] nodes;
/// <summary>Bounding Box Tree for node lookups</summary>
public BBTree bbTree;
/// <summary>Temporary flag used for batching</summary>
public bool flag;
/// <summary>The graph which contains this tile</summary>
public NavmeshBase graph;
#region INavmeshHolder implementation
public void GetTileCoordinates (int tileIndex, out int x, out int z) {
x = this.x;
z = this.z;
}
public int GetVertexArrayIndex (int index) {
return index & NavmeshBase.VertexIndexMask;
}
/// <summary>Get a specific vertex in the tile</summary>
public Int3 GetVertex (int index) {
int idx = index & NavmeshBase.VertexIndexMask;
return verts[idx];
}
public Int3 GetVertexInGraphSpace (int index) {
return vertsInGraphSpace[index & NavmeshBase.VertexIndexMask];
}
/// <summary>Transforms coordinates from graph space to world space</summary>
public GraphTransform transform { get { return graph.transform; } }
#endregion
public void GetNodes (System.Action<GraphNode> action) {
if (nodes == null) return;
for (int i = 0; i < nodes.Length; i++) action(nodes[i]);
}
public void Dispose () {
unsafe {
bbTree.Dispose();
vertsInGraphSpace.Free(Allocator.Persistent);
verts.Free(Allocator.Persistent);
tris.Free(Allocator.Persistent);
preCutTags.Free(Allocator.Persistent);
preCutVertsInTileSpace.Free(Allocator.Persistent);
preCutTris.Free(Allocator.Persistent);
// Ensure Dispose is idempotent
vertsInGraphSpace = default;
verts = default;
tris = default;
preCutTags = default;
preCutVertsInTileSpace = default;
preCutTris = default;
isCut = false;
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7408cbadf2e744d22853a92b15abede1
timeCreated: 1474405146
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using Pathfinding.Graphs.Navmesh.Jobs;
using Pathfinding.Collections;
namespace Pathfinding.Graphs.Navmesh {
/// <summary>Helper methods for scanning a recast graph</summary>
public struct RecastBuilder {
/// <summary>
/// Builds meshes for the given tiles in a graph.
/// Call Schedule on the returned object to actually start the job.
///
/// You may want to adjust the settings on the returned object before calling Schedule.
///
/// <code>
/// // Scans the first 6x6 chunk of tiles of the recast graph (the IntRect uses inclusive coordinates)
/// var graph = AstarPath.active.data.recastGraph;
/// var buildSettings = RecastBuilder.BuildTileMeshes(graph, new TileLayout(graph), new IntRect(0, 0, 5, 5));
/// var disposeArena = new Pathfinding.Jobs.DisposeArena();
/// var promise = buildSettings.Schedule(disposeArena);
///
/// AstarPath.active.AddWorkItem(() => {
/// // Block until the asynchronous job completes
/// var result = promise.Complete();
/// TileMeshes tiles = result.tileMeshes.ToManaged();
/// // Take the scanned tiles and place them in the graph,
/// // but not at their original location, but 2 tiles away, rotated 90 degrees.
/// tiles.tileRect = tiles.tileRect.Offset(new Vector2Int(2, 0));
/// tiles.Rotate(1);
/// graph.ReplaceTiles(tiles);
///
/// // Dispose unmanaged data
/// disposeArena.DisposeAll();
/// result.Dispose();
/// });
/// </code>
/// </summary>
public static TileBuilder BuildTileMeshes (RecastGraph graph, TileLayout tileLayout, IntRect tileRect) {
return new TileBuilder(graph, tileLayout, tileRect);
}
/// <summary>
/// Builds nodes given some tile meshes.
/// Call Schedule on the returned object to actually start the job.
///
/// See: <see cref="BuildTileMeshes"/>
/// </summary>
public static JobBuildNodes BuildNodeTiles (RecastGraph graph, TileLayout tileLayout) {
return new JobBuildNodes(graph, tileLayout);
}
public static TileCutter CutTiles (NavmeshBase graph, GridLookup<NavmeshClipper> cuts, TileLayout tileLayout) {
return new TileCutter(graph, cuts, tileLayout);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6b7a26d35ca0154fa87ac69a555cce1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b37acf4e486d51b8394c1d8e2b0c59c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,367 @@
using System.Collections.Generic;
using Pathfinding.Graphs.Navmesh.Jobs;
using Pathfinding.Jobs;
using Pathfinding.Pooling;
using Pathfinding.Sync;
using Pathfinding.Graphs.Navmesh.Voxelization.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.Assertions;
namespace Pathfinding.Graphs.Navmesh {
/// <summary>
/// Settings for building tile meshes in a recast graph.
///
/// See: <see cref="RecastGraph"/> for more documentation on the individual fields.
/// See: <see cref="RecastBuilder"/>
/// </summary>
public struct TileBuilder {
public float walkableClimb;
public RecastGraph.CollectionSettings collectionSettings;
public RecastGraph.RelevantGraphSurfaceMode relevantGraphSurfaceMode;
public RecastGraph.DimensionMode dimensionMode;
public RecastGraph.BackgroundTraversability backgroundTraversability;
// TODO: Don't store in struct
public int tileBorderSizeInVoxels;
public float walkableHeight;
public float maxSlope;
// TODO: Specify in world units
public int characterRadiusInVoxels;
public int minRegionSize;
public float maxEdgeLength;
public float contourMaxError;
public UnityEngine.SceneManagement.Scene scene;
public TileLayout tileLayout;
public IntRect tileRect;
public List<RecastGraph.PerLayerModification> perLayerModifications;
public class TileBuilderOutput : IProgress, System.IDisposable {
public NativeReference<int> currentTileCounter;
public TileMeshesUnsafe tileMeshes;
#if UNITY_EDITOR
public List<(UnityEngine.Object, Mesh)> meshesUnreadableAtRuntime;
#endif
public float Progress {
get {
var tileCount = tileMeshes.tileRect.Area;
var currentTile = Mathf.Min(tileCount, currentTileCounter.Value);
return tileCount > 0 ? currentTile / (float)tileCount : 0; // "Scanning tiles: " + currentTile + " of " + (tileCount) + " tiles...");
}
}
public void Dispose () {
tileMeshes.Dispose(Allocator.Persistent);
if (currentTileCounter.IsCreated) currentTileCounter.Dispose();
#if UNITY_EDITOR
if (meshesUnreadableAtRuntime != null) ListPool<(UnityEngine.Object, Mesh)>.Release(ref meshesUnreadableAtRuntime);
#endif
}
}
public TileBuilder (RecastGraph graph, TileLayout tileLayout, IntRect tileRect) {
this.tileLayout = tileLayout;
this.tileRect = tileRect;
// A walkableClimb higher than walkableHeight can cause issues when generating the navmesh since then it can in some cases
// Both be valid for a character to walk under an obstacle and climb up on top of it (and that cannot be handled with navmesh without links)
// The editor scripts also enforce this, but we enforce it here too just to be sure
this.walkableClimb = Mathf.Min(graph.walkableClimb, graph.walkableHeight);
this.collectionSettings = graph.collectionSettings;
this.dimensionMode = graph.dimensionMode;
this.backgroundTraversability = graph.backgroundTraversability;
this.tileBorderSizeInVoxels = graph.TileBorderSizeInVoxels;
this.walkableHeight = graph.walkableHeight;
this.maxSlope = graph.maxSlope;
this.characterRadiusInVoxels = graph.CharacterRadiusInVoxels;
this.minRegionSize = Mathf.RoundToInt(graph.minRegionSize);
this.maxEdgeLength = graph.maxEdgeLength;
this.contourMaxError = graph.contourMaxError;
this.relevantGraphSurfaceMode = graph.relevantGraphSurfaceMode;
this.scene = graph.active.gameObject.scene;
this.perLayerModifications = graph.perLayerModifications;
}
/// <summary>
/// Number of extra voxels on each side of a tile to ensure accurate navmeshes near the tile border.
/// The width of a tile is expanded by 2 times this value (1x to the left and 1x to the right)
/// </summary>
int TileBorderSizeInVoxels {
get {
return characterRadiusInVoxels + 3;
}
}
float TileBorderSizeInWorldUnits {
get {
return TileBorderSizeInVoxels*tileLayout.cellSize;
}
}
/// <summary>Get the world space bounds for all tiles, including an optional (graph space) padding around the tiles in the x and z axis</summary>
public Bounds GetWorldSpaceBounds (float xzPadding = 0) {
var graphSpaceBounds = tileLayout.GetTileBoundsInGraphSpace(tileRect.xmin, tileRect.ymin, tileRect.Width, tileRect.Height);
graphSpaceBounds.Expand(new Vector3(2*xzPadding, 0, 2*xzPadding));
return tileLayout.transform.Transform(graphSpaceBounds);
}
public RecastMeshGatherer.MeshCollection CollectMeshes (Bounds bounds) {
Profiler.BeginSample("Find Meshes for rasterization");
var mask = collectionSettings.layerMask;
var tagMask = collectionSettings.tagMask;
if (collectionSettings.collectionMode == RecastGraph.CollectionSettings.FilterMode.Layers) {
tagMask = null;
} else {
mask = -1;
}
var meshGatherer = new RecastMeshGatherer(scene, bounds, collectionSettings.terrainHeightmapDownsamplingFactor, collectionSettings.layerMask, collectionSettings.tagMask, perLayerModifications, tileLayout.cellSize / collectionSettings.colliderRasterizeDetail);
if (collectionSettings.rasterizeMeshes && dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
Profiler.BeginSample("Find meshes");
meshGatherer.CollectSceneMeshes();
Profiler.EndSample();
}
Profiler.BeginSample("Find RecastNavmeshModifiers");
meshGatherer.CollectRecastNavmeshModifiers();
Profiler.EndSample();
if (collectionSettings.rasterizeTerrain && dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
Profiler.BeginSample("Find terrains");
// Split terrains up into meshes approximately the size of a single tile
var desiredTerrainChunkSize = 0.51f * tileLayout.cellSize*(math.max(tileLayout.tileSizeInVoxels.x, tileLayout.tileSizeInVoxels.y) + 2*TileBorderSizeInVoxels);
meshGatherer.CollectTerrainMeshes(collectionSettings.rasterizeTrees, desiredTerrainChunkSize);
Profiler.EndSample();
}
if (collectionSettings.rasterizeColliders || dimensionMode == RecastGraph.DimensionMode.Dimension2D) {
Profiler.BeginSample("Find colliders");
if (dimensionMode == RecastGraph.DimensionMode.Dimension3D) {
meshGatherer.CollectColliderMeshes();
} else {
meshGatherer.Collect2DColliderMeshes();
}
Profiler.EndSample();
}
if (collectionSettings.onCollectMeshes != null) {
Profiler.BeginSample("Custom mesh collection");
collectionSettings.onCollectMeshes(meshGatherer);
Profiler.EndSample();
}
Profiler.BeginSample("Finalizing");
var result = meshGatherer.Finalize();
Profiler.EndSample();
// Warn if no meshes were found, but only if the tile rect covers the whole graph.
// If it's just a partial update, the user is probably not interested in this warning,
// as it is completely normal that there are some empty tiles.
if (tileRect == new IntRect(0, 0, tileLayout.tileCount.x - 1, tileLayout.tileCount.y - 1) && result.meshes.Length == 0) {
Debug.LogWarning("No rasterizable objects were found contained in the layers specified by the 'mask' variables");
}
Profiler.EndSample();
return result;
}
/// <summary>A mapping from tiles to the meshes that each tile touches</summary>
public struct BucketMapping {
/// <summary>All meshes that should be voxelized</summary>
public NativeArray<RasterizationMesh> meshes;
/// <summary>Indices into the <see cref="meshes"/> array</summary>
public NativeArray<int> pointers;
/// <summary>
/// For each tile, the range of pointers in <see cref="pointers"/> that correspond to that tile.
/// This is a cumulative sum of the number of pointers in each bucket.
///
/// Bucket i will contain pointers in the range [i > 0 ? bucketRanges[i-1] : 0, bucketRanges[i]).
///
/// The length is the same as the number of tiles.
/// </summary>
public NativeArray<int> bucketRanges;
}
/// <summary>Creates a list for every tile and adds every mesh that touches a tile to the corresponding list</summary>
BucketMapping PutMeshesIntoTileBuckets (RecastMeshGatherer.MeshCollection meshCollection, IntRect tileBuckets) {
var bucketCount = tileBuckets.Width*tileBuckets.Height;
var buckets = new NativeList<int>[bucketCount];
var borderExpansion = TileBorderSizeInWorldUnits;
for (int i = 0; i < buckets.Length; i++) {
buckets[i] = new NativeList<int>(Allocator.Persistent);
}
var offset = -tileBuckets.Min;
var clamp = new IntRect(0, 0, tileBuckets.Width - 1, tileBuckets.Height - 1);
var meshes = meshCollection.meshes;
for (int i = 0; i < meshes.Length; i++) {
var mesh = meshes[i];
var bounds = mesh.bounds;
var rect = tileLayout.GetTouchingTiles(bounds, borderExpansion);
rect = IntRect.Intersection(rect.Offset(offset), clamp);
for (int z = rect.ymin; z <= rect.ymax; z++) {
for (int x = rect.xmin; x <= rect.xmax; x++) {
buckets[x + z*tileBuckets.Width].Add(i);
}
}
}
// Concat buckets
int allPointersCount = 0;
for (int i = 0; i < buckets.Length; i++) allPointersCount += buckets[i].Length;
var allPointers = new NativeArray<int>(allPointersCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
var bucketRanges = new NativeArray<int>(bucketCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
allPointersCount = 0;
for (int i = 0; i < buckets.Length; i++) {
// If we have an empty bucket at the end of the array then allPointersCount might be equal to allPointers.Length which would cause an assert to trigger.
// So for empty buckets don't call the copy method
if (buckets[i].Length > 0) {
NativeArray<int>.Copy(buckets[i].AsArray(), 0, allPointers, allPointersCount, buckets[i].Length);
}
allPointersCount += buckets[i].Length;
bucketRanges[i] = allPointersCount;
buckets[i].Dispose();
}
return new BucketMapping {
meshes = meshCollection.meshes,
pointers = allPointers,
bucketRanges = bucketRanges,
};
}
public Promise<TileBuilderOutput> Schedule (DisposeArena arena) {
var tileCount = tileRect.Area;
Assert.IsTrue(tileCount > 0);
var tileRectWidth = tileRect.Width;
var tileRectDepth = tileRect.Height;
// Find all meshes that could affect the graph
var worldBounds = GetWorldSpaceBounds(TileBorderSizeInWorldUnits);
if (dimensionMode == RecastGraph.DimensionMode.Dimension2D) {
// In 2D mode, the bounding box of the graph only bounds it in the X and Y dimensions
worldBounds.extents = new Vector3(worldBounds.extents.x, worldBounds.extents.y, float.PositiveInfinity);
}
var meshes = CollectMeshes(worldBounds);
Profiler.BeginSample("PutMeshesIntoTileBuckets");
var buckets = PutMeshesIntoTileBuckets(meshes, tileRect);
Profiler.EndSample();
Profiler.BeginSample("Allocating tiles");
var tileMeshes = new NativeArray<TileMesh.TileMeshUnsafe>(tileCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
int width = tileLayout.tileSizeInVoxels.x + tileBorderSizeInVoxels*2;
int depth = tileLayout.tileSizeInVoxels.y + tileBorderSizeInVoxels*2;
var cellHeight = tileLayout.CellHeight;
// TODO: Move inside BuildTileMeshBurst
var voxelWalkableHeight = (uint)(walkableHeight/cellHeight);
var voxelWalkableClimb = Mathf.RoundToInt(walkableClimb/cellHeight);
var tileGraphSpaceBounds = new NativeArray<Bounds>(tileCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
for (int z = 0; z < tileRectDepth; z++) {
for (int x = 0; x < tileRectWidth; x++) {
int tileIndex = x + z*tileRectWidth;
var tileBounds = tileLayout.GetTileBoundsInGraphSpace(tileRect.xmin + x, tileRect.ymin + z);
// Expand borderSize voxels on each side
tileBounds.Expand(new Vector3(1, 0, 1)*TileBorderSizeInWorldUnits*2);
tileGraphSpaceBounds[tileIndex] = tileBounds;
}
}
Profiler.EndSample();
Profiler.BeginSample("Scheduling jobs");
var builders = new TileBuilderBurst[Mathf.Max(1, Mathf.Min(tileCount, Unity.Jobs.LowLevel.Unsafe.JobsUtility.JobWorkerCount + 1))];
var currentTileCounter = new NativeReference<int>(0, Allocator.Persistent);
JobHandle dependencies = default;
var relevantGraphSurfaces = new NativeList<JobBuildRegions.RelevantGraphSurfaceInfo>(Allocator.Persistent);
var c = RelevantGraphSurface.Root;
while (c != null) {
relevantGraphSurfaces.Add(new JobBuildRegions.RelevantGraphSurfaceInfo {
position = c.transform.position,
range = c.maxRange,
});
c = c.Next;
}
// Having a few long running jobs is bad because Unity cannot inject more high priority jobs
// in between tile calculations. So we run each builder a number of times.
// Each step will just calculate one tile.
int tilesPerJob = Mathf.CeilToInt(Mathf.Sqrt(tileCount));
// Number of tiles calculated if every builder runs once
int tilesPerStep = tilesPerJob * builders.Length;
// Round up to make sure we run the jobs enough times
// We multiply by 2 to run a bit more jobs than strictly necessary.
// This is to ensure that if one builder just gets a bunch of long running jobs
// then the other builders can steal some work from it.
int jobSteps = 2 * (tileCount + tilesPerStep - 1) / tilesPerStep;
var jobTemplate = new JobBuildTileMeshFromVoxels {
tileBuilder = builders[0],
inputMeshes = buckets,
tileGraphSpaceBounds = tileGraphSpaceBounds,
voxelWalkableClimb = voxelWalkableClimb,
voxelWalkableHeight = voxelWalkableHeight,
voxelToTileSpace = Matrix4x4.Scale(new Vector3(tileLayout.cellSize, cellHeight, tileLayout.cellSize)) * Matrix4x4.Translate(-new Vector3(1, 0, 1)*TileBorderSizeInVoxels),
cellSize = tileLayout.cellSize,
cellHeight = cellHeight,
maxSlope = Mathf.Max(maxSlope, 0.0001f), // Ensure maxSlope is not 0, as then horizontal surfaces can sometimes get excluded due to floating point errors
dimensionMode = dimensionMode,
backgroundTraversability = backgroundTraversability,
graphToWorldSpace = tileLayout.transform.matrix,
// Crop all tiles to ensure they are inside the graph bounds (even if the tiles did not line up perfectly with the bounding box).
// Add the character radius, since it will be eroded away anyway, but subtract 1 voxel to ensure the nodes are strictly inside the bounding box
graphSpaceLimits = new Vector2(tileLayout.graphSpaceSize.x + (characterRadiusInVoxels-1)*tileLayout.cellSize, tileLayout.graphSpaceSize.z + (characterRadiusInVoxels-1)*tileLayout.cellSize),
characterRadiusInVoxels = characterRadiusInVoxels,
tileBorderSizeInVoxels = tileBorderSizeInVoxels,
minRegionSize = minRegionSize,
maxEdgeLength = maxEdgeLength,
contourMaxError = contourMaxError,
maxTiles = tilesPerJob,
relevantGraphSurfaces = relevantGraphSurfaces.AsArray(),
relevantGraphSurfaceMode = this.relevantGraphSurfaceMode,
};
jobTemplate.SetOutputMeshes(tileMeshes);
jobTemplate.SetCounter(currentTileCounter);
int maximumVoxelYCoord = (int)(tileLayout.graphSpaceSize.y / cellHeight);
for (int i = 0; i < builders.Length; i++) {
jobTemplate.tileBuilder = builders[i] = new TileBuilderBurst(width, depth, (int)voxelWalkableHeight, maximumVoxelYCoord);
var dep = new JobHandle();
for (int j = 0; j < jobSteps; j++) {
dep = jobTemplate.Schedule(dep);
}
dependencies = JobHandle.CombineDependencies(dependencies, dep);
}
JobHandle.ScheduleBatchedJobs();
Profiler.EndSample();
arena.Add(tileGraphSpaceBounds);
arena.Add(relevantGraphSurfaces);
arena.Add(buckets.bucketRanges);
arena.Add(buckets.pointers);
// Note: buckets.meshes references data in #meshes, so we don't have to dispose it separately
arena.Add(meshes);
// Dispose the mesh data after all jobs are completed.
// Note that the jobs use pointers to this data which are not tracked by the safety system.
for (int i = 0; i < builders.Length; i++) arena.Add(builders[i]);
return new Promise<TileBuilderOutput>(dependencies, new TileBuilderOutput {
tileMeshes = new TileMeshesUnsafe(tileMeshes, tileRect, new Vector2(tileLayout.TileWorldSizeX, tileLayout.TileWorldSizeZ)),
currentTileCounter = currentTileCounter,
#if UNITY_EDITOR
meshesUnreadableAtRuntime = meshes.meshesUnreadableAtRuntime,
#endif
});
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1bfefed1cddc88f449cc850ad00f2f77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 664ab28b7671144dfa4515ea79a4c49e
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@@ -0,0 +1,112 @@
using UnityEngine;
using Pathfinding.Util;
using UnityEngine.Tilemaps;
namespace Pathfinding.Graphs.Navmesh {
/// <summary>
/// Represents the position and size of a tile grid for a recast/navmesh graph.
///
/// This separates out the physical layout of tiles from all the other recast graph settings.
/// </summary>
public struct TileLayout {
/// <summary>How many tiles there are in the grid</summary>
public Vector2Int tileCount;
/// <summary>Transforms coordinates from graph space to world space</summary>
public GraphTransform transform;
/// <summary>Size of a tile in voxels along the X and Z axes</summary>
public Vector2Int tileSizeInVoxels;
/// <summary>
/// Size in graph space of the whole grid.
///
/// If the original bounding box was not an exact multiple of the tile size, this will be less than the total width of all tiles.
/// </summary>
public Vector3 graphSpaceSize;
/// <summary>\copydocref{RecastGraph.cellSize}</summary>
public float cellSize;
/// <summary>
/// Voxel y coordinates will be stored as ushorts which have 65536 values.
/// Leave a margin to make sure things do not overflow
/// </summary>
public float CellHeight => Mathf.Max(graphSpaceSize.y / 64000, 0.001f);
public Vector2 TileWorldSize => new Vector2(TileWorldSizeX, TileWorldSizeZ);
/// <summary>Size of a tile in world units, along the graph's X axis</summary>
public float TileWorldSizeX => tileSizeInVoxels.x * cellSize;
/// <summary>Size of a tile in world units, along the graph's Z axis</summary>
public float TileWorldSizeZ => tileSizeInVoxels.y * cellSize;
/// <summary>Returns an XZ bounds object with the bounds of a group of tiles in graph space</summary>
public Bounds GetTileBoundsInGraphSpace (int x, int z, int width = 1, int depth = 1) {
var bounds = new Bounds();
bounds.SetMinMax(new Vector3(x*TileWorldSizeX, 0, z*TileWorldSizeZ),
new Vector3((x+width)*TileWorldSizeX, graphSpaceSize.y, (z+depth)*TileWorldSizeZ)
);
return bounds;
}
/// <summary>
/// Returns a rect containing the indices of all tiles touching the specified bounds.
/// If a margin is passed, the bounding box in graph space is expanded by that amount in every direction.
/// </summary>
public IntRect GetTouchingTiles (Bounds bounds, float margin = 0) {
bounds = transform.InverseTransform(bounds);
// Calculate world bounds of all affected tiles
return new IntRect(Mathf.FloorToInt((bounds.min.x - margin) / TileWorldSizeX), Mathf.FloorToInt((bounds.min.z - margin) / TileWorldSizeZ), Mathf.FloorToInt((bounds.max.x + margin) / TileWorldSizeX), Mathf.FloorToInt((bounds.max.z + margin) / TileWorldSizeZ));
}
/// <summary>Returns a rect containing the indices of all tiles touching the specified bounds.</summary>
/// <param name="rect">Graph space rectangle (in graph space all tiles are on the XZ plane regardless of graph rotation and other transformations, the first tile has a corner at the origin)</param>
public IntRect GetTouchingTilesInGraphSpace (Rect rect) {
// Calculate world bounds of all affected tiles
var r = new IntRect(Mathf.FloorToInt(rect.xMin / TileWorldSizeX), Mathf.FloorToInt(rect.yMin / TileWorldSizeZ), Mathf.FloorToInt(rect.xMax / TileWorldSizeX), Mathf.FloorToInt(rect.yMax / TileWorldSizeZ));
// Clamp to bounds
r = IntRect.Intersection(r, new IntRect(0, 0, tileCount.x-1, tileCount.y-1));
return r;
}
public TileLayout(RecastGraph graph) : this(new Bounds(graph.forcedBoundsCenter, graph.forcedBoundsSize), Quaternion.Euler(graph.rotation), graph.cellSize, graph.editorTileSize, graph.useTiles) {
}
public TileLayout(NavMeshGraph graph) : this(new Bounds(graph.transform.Transform(graph.forcedBoundsSize*0.5f), graph.forcedBoundsSize), Quaternion.Euler(graph.rotation), 0.001f, 0, false) {
}
public TileLayout(Bounds bounds, Quaternion rotation, float cellSize, int tileSizeInVoxels, bool useTiles) {
this.transform = RecastGraph.CalculateTransform(bounds, rotation);
this.cellSize = cellSize;
// Voxel grid size
var size = bounds.size;
graphSpaceSize = size;
int totalVoxelWidth = (int)(size.x/cellSize + 0.5f);
int totalVoxelDepth = (int)(size.z/cellSize + 0.5f);
if (!useTiles) {
this.tileSizeInVoxels = new Vector2Int(totalVoxelWidth, totalVoxelDepth);
tileCount = new Vector2Int(1, 1);
} else {
this.tileSizeInVoxels = new Vector2Int(tileSizeInVoxels, tileSizeInVoxels);
// Number of tiles
tileCount = new Vector2Int(
Mathf.Max(0, (totalVoxelWidth + this.tileSizeInVoxels.x-1) / this.tileSizeInVoxels.x),
Mathf.Max(0, (totalVoxelDepth + this.tileSizeInVoxels.y-1) / this.tileSizeInVoxels.y)
);
}
if (tileCount.x*tileCount.y > NavmeshBase.TileIndexMask + 1) {
throw new System.Exception("Too many tiles ("+(tileCount.x*tileCount.y)+") maximum is "+(NavmeshBase.TileIndexMask + 1)+
"\nTry disabling ASTAR_RECAST_LARGER_TILES under the 'Optimizations' tab in the A* inspector.");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cf2ae7ff6aabbdc4fa76468eedbf53f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using Pathfinding.Collections;
using Unity.Collections;
namespace Pathfinding.Graphs.Navmesh {
/// <summary>
/// A tile in a navmesh graph.
///
/// This is an intermediate representation used when building the navmesh, and also in some cases for serializing the navmesh to a portable format.
///
/// See: <see cref="NavmeshTile"/> for the representation used for pathfinding.
/// </summary>
public struct TileMesh {
public int[] triangles;
public Int3[] verticesInTileSpace;
/// <summary>One tag per triangle</summary>
public uint[] tags;
/// <summary>Unsafe version of <see cref="TileMesh"/></summary>
public struct TileMeshUnsafe {
/// <summary>Three indices per triangle</summary>
public UnsafeSpan<int> triangles;
/// <summary>One vertex per triangle</summary>
public UnsafeSpan<Int3> verticesInTileSpace;
/// <summary>One tag per triangle</summary>
public UnsafeSpan<uint> tags;
/// <summary>
/// Frees the underlaying memory.
/// This struct should not be used after this method has been called.
///
/// Warning: Only call if you know that the memory is owned by this struct, as it is entirely possible for it to just represent views into other memory.
/// </summary>
public void Dispose (Allocator allocator) {
triangles.Free(allocator);
verticesInTileSpace.Free(allocator);
tags.Free(allocator);
}
public TileMesh ToManaged () {
return new TileMesh {
triangles = triangles.ToArray(),
verticesInTileSpace = verticesInTileSpace.ToArray(),
tags = tags.ToArray(),
};
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16f2efac26c436946b764d2263a0a089
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,193 @@
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
namespace Pathfinding.Graphs.Navmesh {
/// <summary>
/// Represents a rectangular group of tiles of a recast graph.
///
/// This is a portable representation in that it can be serialized to and from a byte array.
///
/// <code>
/// // Scans the first 6x6 chunk of tiles of the recast graph (the IntRect uses inclusive coordinates)
/// var graph = AstarPath.active.data.recastGraph;
/// var buildSettings = RecastBuilder.BuildTileMeshes(graph, new TileLayout(graph), new IntRect(0, 0, 5, 5));
/// var disposeArena = new Pathfinding.Jobs.DisposeArena();
/// var promise = buildSettings.Schedule(disposeArena);
///
/// AstarPath.active.AddWorkItem(() => {
/// // Block until the asynchronous job completes
/// var result = promise.Complete();
/// TileMeshes tiles = result.tileMeshes.ToManaged();
/// // Take the scanned tiles and place them in the graph,
/// // but not at their original location, but 2 tiles away, rotated 90 degrees.
/// tiles.tileRect = tiles.tileRect.Offset(new Vector2Int(2, 0));
/// tiles.Rotate(1);
/// graph.ReplaceTiles(tiles);
///
/// // Dispose unmanaged data
/// disposeArena.DisposeAll();
/// result.Dispose();
/// });
/// </code>
///
/// See: <see cref="NavmeshPrefab"/> uses this representation internally for storage.
/// See: <see cref="RecastGraph.ReplaceTiles"/>
/// See: <see cref="RecastBuilder.BuildTileMeshes"/>
/// </summary>
public struct TileMeshes {
/// <summary>Tiles laid out row by row</summary>
public TileMesh[] tileMeshes;
/// <summary>Which tiles in the graph this group of tiles represents</summary>
public IntRect tileRect;
/// <summary>World-space size of each tile</summary>
public Vector2 tileWorldSize;
/// <summary>Rotate this group of tiles by 90*N degrees clockwise about the group's center</summary>
public void Rotate (int rotation) {
rotation = -rotation;
// Get the positive remainder modulo 4. I.e. a number between 0 and 3.
rotation = ((rotation % 4) + 4) % 4;
if (rotation == 0) return;
var rot90 = new int2x2(0, -1, 1, 0);
var rotN = int2x2.identity;
for (int i = 0; i < rotation; i++) rotN = math.mul(rotN, rot90);
var tileSize = (Int3) new Vector3(tileWorldSize.x, 0, tileWorldSize.y);
var offset = -math.min(int2.zero, math.mul(rotN, new int2(tileSize.x, tileSize.z)));
var size = new int2(tileRect.Width, tileRect.Height);
var offsetTileCoordinate = -math.min(int2.zero, math.mul(rotN, size - 1));
var newTileMeshes = new TileMesh[tileMeshes.Length];
var newSize = (rotation % 2) == 0 ? size : new int2(size.y, size.x);
for (int z = 0; z < size.y; z++) {
for (int x = 0; x < size.x; x++) {
var vertices = tileMeshes[x + z*size.x].verticesInTileSpace;
for (int i = 0; i < vertices.Length; i++) {
var v = vertices[i];
var rotated = math.mul(rotN, new int2(v.x, v.z)) + offset;
vertices[i] = new Int3(rotated.x, v.y, rotated.y);
}
var tileCoord = math.mul(rotN, new int2(x, z)) + offsetTileCoordinate;
newTileMeshes[tileCoord.x + tileCoord.y*newSize.x] = tileMeshes[x + z*size.x];
}
}
tileMeshes = newTileMeshes;
tileWorldSize = rotation % 2 == 0 ? tileWorldSize : new Vector2(tileWorldSize.y, tileWorldSize.x);
tileRect = new IntRect(tileRect.xmin, tileRect.ymin, tileRect.xmin + newSize.x - 1, tileRect.ymin + newSize.y - 1);
}
/// <summary>
/// Serialize this struct to a portable byte array.
/// The data is compressed using the deflate algorithm to reduce size.
/// See: <see cref="Deserialize"/>
/// </summary>
public byte[] Serialize () {
var buffer = new System.IO.MemoryStream();
var writer = new System.IO.BinaryWriter(new System.IO.Compression.DeflateStream(buffer, System.IO.Compression.CompressionMode.Compress));
// Version
writer.Write(0);
writer.Write(tileRect.Width);
writer.Write(tileRect.Height);
writer.Write(this.tileWorldSize.x);
writer.Write(this.tileWorldSize.y);
for (int z = 0; z < tileRect.Height; z++) {
for (int x = 0; x < tileRect.Width; x++) {
var tile = tileMeshes[(z*tileRect.Width) + x];
UnityEngine.Assertions.Assert.IsTrue(tile.tags.Length*3 == tile.triangles.Length);
writer.Write(tile.triangles.Length);
writer.Write(tile.verticesInTileSpace.Length);
for (int i = 0; i < tile.verticesInTileSpace.Length; i++) {
var v = tile.verticesInTileSpace[i];
writer.Write(v.x);
writer.Write(v.y);
writer.Write(v.z);
}
for (int i = 0; i < tile.triangles.Length; i++) writer.Write(tile.triangles[i]);
for (int i = 0; i < tile.tags.Length; i++) writer.Write(tile.tags[i]);
}
}
writer.Close();
return buffer.ToArray();
}
/// <summary>
/// Deserialize an instance from a byte array.
/// See: <see cref="Serialize"/>
/// </summary>
public static TileMeshes Deserialize (byte[] bytes) {
var reader = new System.IO.BinaryReader(new System.IO.Compression.DeflateStream(new System.IO.MemoryStream(bytes), System.IO.Compression.CompressionMode.Decompress));
var version = reader.ReadInt32();
if (version != 0) throw new System.Exception("Invalid data. Unexpected version number.");
var w = reader.ReadInt32();
var h = reader.ReadInt32();
var tileSize = new Vector2(reader.ReadSingle(), reader.ReadSingle());
if (w < 0 || h < 0) throw new System.Exception("Invalid bounds");
var tileRect = new IntRect(0, 0, w - 1, h - 1);
var tileMeshes = new TileMesh[w*h];
for (int z = 0; z < h; z++) {
for (int x = 0; x < w; x++) {
int[] tris = new int[reader.ReadInt32()];
Int3[] vertsInTileSpace = new Int3[reader.ReadInt32()];
uint[] tags = new uint[tris.Length/3];
for (int i = 0; i < vertsInTileSpace.Length; i++) vertsInTileSpace[i] = new Int3(reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32());
for (int i = 0; i < tris.Length; i++) {
tris[i] = reader.ReadInt32();
UnityEngine.Assertions.Assert.IsTrue(tris[i] >= 0 && tris[i] < vertsInTileSpace.Length);
}
for (int i = 0; i < tags.Length; i++) tags[i] = reader.ReadUInt32();
tileMeshes[x + z*w] = new TileMesh {
triangles = tris,
verticesInTileSpace = vertsInTileSpace,
tags = tags,
};
}
}
return new TileMeshes {
tileMeshes = tileMeshes,
tileRect = tileRect,
tileWorldSize = tileSize,
};
}
}
/// <summary>Unsafe representation of a <see cref="TileMeshes"/> struct</summary>
public struct TileMeshesUnsafe {
public NativeArray<TileMesh.TileMeshUnsafe> tileMeshes;
public IntRect tileRect;
public Vector2 tileWorldSize;
public TileMeshesUnsafe(NativeArray<TileMesh.TileMeshUnsafe> tileMeshes, IntRect tileRect, Vector2 tileWorldSize) {
this.tileMeshes = tileMeshes;
this.tileRect = tileRect;
this.tileWorldSize = tileWorldSize;
}
/// <summary>Copies the native data to managed data arrays which are easier to work with</summary>
public TileMeshes ToManaged () {
var output = new TileMesh[tileMeshes.Length];
for (int i = 0; i < output.Length; i++) {
output[i] = tileMeshes[i].ToManaged();
}
return new TileMeshes {
tileMeshes = output,
tileRect = this.tileRect,
tileWorldSize = this.tileWorldSize,
};
}
public void Dispose (Allocator allocator) {
// Allows calling Dispose on zero-initialized instances
if (!tileMeshes.IsCreated) return;
for (int i = 0; i < tileMeshes.Length; i++) tileMeshes[i].Dispose(allocator);
tileMeshes.Dispose();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8e1e88f7c3e2d2c45ab0ba43bbce2cd4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ce1c1f6432f234a46b5e914d99379d70

View File

@@ -0,0 +1,132 @@
using Pathfinding.Jobs;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine.Assertions;
namespace Pathfinding.Graphs.Navmesh.Voxelization.Burst {
/// <summary>Stores a compact voxel field. </summary>
public struct CompactVoxelField : IArenaDisposable {
public const int UnwalkableArea = 0;
public const uint NotConnected = 0x3f;
public readonly int voxelWalkableHeight;
public readonly int width, depth;
public NativeList<CompactVoxelSpan> spans;
public NativeList<CompactVoxelCell> cells;
public NativeList<int> areaTypes;
/// <summary>Unmotivated variable, but let's clamp the layers at 65535</summary>
public const int MaxLayers = 65535;
public CompactVoxelField (int width, int depth, int voxelWalkableHeight, Allocator allocator) {
spans = new NativeList<CompactVoxelSpan>(0, allocator);
cells = new NativeList<CompactVoxelCell>(0, allocator);
areaTypes = new NativeList<int>(0, allocator);
this.width = width;
this.depth = depth;
this.voxelWalkableHeight = voxelWalkableHeight;
}
void IArenaDisposable.DisposeWith (DisposeArena arena) {
arena.Add(spans);
arena.Add(cells);
arena.Add(areaTypes);
}
public int GetNeighbourIndex (int index, int direction) {
return index + VoxelUtilityBurst.DX[direction] + VoxelUtilityBurst.DZ[direction] * width;
}
public void BuildFromLinkedField (LinkedVoxelField field) {
int idx = 0;
Assert.AreEqual(this.width, field.width);
Assert.AreEqual(this.depth, field.depth);
int w = field.width;
int d = field.depth;
int wd = w*d;
int spanCount = field.GetSpanCount();
spans.Resize(spanCount, NativeArrayOptions.UninitializedMemory);
areaTypes.Resize(spanCount, NativeArrayOptions.UninitializedMemory);
cells.Resize(wd, NativeArrayOptions.UninitializedMemory);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if (this.voxelWalkableHeight >= ushort.MaxValue) {
throw new System.Exception("Too high walkable height to guarantee correctness. Increase voxel height or lower walkable height.");
}
#endif
var linkedSpans = field.linkedSpans;
for (int z = 0; z < wd; z += w) {
for (int x = 0; x < w; x++) {
int spanIndex = x+z;
if (linkedSpans[spanIndex].bottom == LinkedVoxelField.InvalidSpanValue) {
cells[x+z] = new CompactVoxelCell(0, 0);
continue;
}
int index = idx;
int count = 0;
while (spanIndex != -1) {
if (linkedSpans[spanIndex].area != UnwalkableArea) {
int bottom = (int)linkedSpans[spanIndex].top;
int next = linkedSpans[spanIndex].next;
int top = next != -1 ? (int)linkedSpans[next].bottom : LinkedVoxelField.MaxHeightInt;
// TODO: Why is top-bottom clamped to a ushort range?
spans[idx] = new CompactVoxelSpan((ushort)math.min(bottom, ushort.MaxValue), (uint)math.min(top-bottom, ushort.MaxValue));
areaTypes[idx] = linkedSpans[spanIndex].area;
idx++;
count++;
}
spanIndex = linkedSpans[spanIndex].next;
}
cells[x+z] = new CompactVoxelCell(index, count);
}
}
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if (idx != spanCount) throw new System.Exception("Found span count does not match expected value");
#endif
}
}
/// <summary>CompactVoxelCell used for recast graphs.</summary>
public struct CompactVoxelCell {
public int index;
public int count;
public CompactVoxelCell (int i, int c) {
index = i;
count = c;
}
}
/// <summary>CompactVoxelSpan used for recast graphs.</summary>
public struct CompactVoxelSpan {
public ushort y;
public uint con;
public uint h;
public int reg;
public CompactVoxelSpan (ushort bottom, uint height) {
con = 24;
y = bottom;
h = height;
reg = 0;
}
public void SetConnection (int dir, uint value) {
int shift = dir*6;
con = (uint)((con & ~(0x3f << shift)) | ((value & 0x3f) << shift));
}
public int GetConnection (int dir) {
return ((int)con >> dir*6) & 0x3f;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ddc46f5b05337b6ba8eae5dd4906634d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,295 @@
using Pathfinding.Jobs;
using Unity.Collections;
using Unity.Mathematics;
namespace Pathfinding.Graphs.Navmesh.Voxelization.Burst {
struct CellMinMax {
public int objectID;
public int min;
public int max;
}
public struct LinkedVoxelField : IArenaDisposable {
public const uint MaxHeight = 65536;
public const int MaxHeightInt = 65536;
/// <summary>
/// Constant for default LinkedVoxelSpan top and bottom values.
/// It is important with the U since ~0 != ~0U
/// This can be used to check if a LinkedVoxelSpan is valid and not just the default span
/// </summary>
public const uint InvalidSpanValue = ~0U;
/// <summary>Initial estimate on the average number of spans (layers) in the voxel representation. Should be greater or equal to 1</summary>
public const float AvgSpanLayerCountEstimate = 8;
/// <summary>The width of the field along the x-axis. [Limit: >= 0] [Units: voxels]</summary>
public int width;
/// <summary>The depth of the field along the z-axis. [Limit: >= 0] [Units: voxels]</summary>
public int depth;
/// <summary>The maximum height coordinate. [Limit: >= 0, <= MaxHeight] [Units: voxels]</summary>
public int height;
public bool flatten;
public NativeList<LinkedVoxelSpan> linkedSpans;
private NativeList<int> removedStack;
private NativeList<CellMinMax> linkedCellMinMax;
public LinkedVoxelField (int width, int depth, int height) {
this.width = width;
this.depth = depth;
this.height = height;
this.flatten = true;
linkedSpans = new NativeList<LinkedVoxelSpan>(0, Allocator.Persistent);
removedStack = new NativeList<int>(128, Allocator.Persistent);
linkedCellMinMax = new NativeList<CellMinMax>(0, Allocator.Persistent);
}
void IArenaDisposable.DisposeWith (DisposeArena arena) {
arena.Add(linkedSpans);
arena.Add(removedStack);
arena.Add(linkedCellMinMax);
}
public void ResetLinkedVoxelSpans () {
int len = width * depth;
LinkedVoxelSpan df = new LinkedVoxelSpan(InvalidSpanValue, InvalidSpanValue, -1, -1);
linkedSpans.ResizeUninitialized(len);
linkedCellMinMax.Resize(len, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < len; i++) {
linkedSpans[i] = df;
linkedCellMinMax[i] = new CellMinMax {
objectID = -1,
min = 0,
max = 0,
};
}
removedStack.Clear();
}
void PushToSpanRemovedStack (int index) {
removedStack.Add(index);
}
public int GetSpanCount () {
int count = 0;
int wd = width*depth;
for (int x = 0; x < wd; x++) {
for (int s = x; s != -1 && linkedSpans[s].bottom != InvalidSpanValue; s = linkedSpans[s].next) {
count += linkedSpans[s].area != 0 ? 1 : 0;
}
}
return count;
}
public void ResolveSolid (int index, int objectID, int voxelWalkableClimb) {
var minmax = linkedCellMinMax[index];
if (minmax.objectID != objectID) return;
if (minmax.min < minmax.max - 1) {
// Add a span for the solid part of the object.
//
// This span ends at max-1 (where max is the top of the original object).
// This is to avoid issues when merging spans with different areas.
// Assume we had 3 spans like:
// y=0..5 walkable span from another object, area=2
// y=9..10 walkable span, area=3
// and min=0, max=10 for the current object.
// If we added a span for the whole solid range (0..10), then it will first get merged with the 0..5 span, receiving its area (assuming walkable climb was high enough),
// and then get merged with the 9..10 span, replacing its area. This would make the final area be 2, instead of 3 like it should be.
// If we instead add a solid span for the range 0..9, then the tie breaking will ensure that the final area is 3.
// Spans are always at least 1 voxel tall, so the solid span will always get merged with the original span.
AddLinkedSpan(index, minmax.min, minmax.max-1, CompactVoxelField.UnwalkableArea, voxelWalkableClimb, objectID);
}
}
public void SetWalkableBackground () {
int wd = width*depth;
for (int i = 0; i < wd; i++) {
linkedSpans[i] = new LinkedVoxelSpan(0, 1, 1);
}
}
public void AddFlattenedSpan (int index, int area) {
if (linkedSpans[index].bottom == InvalidSpanValue) {
linkedSpans[index] = new LinkedVoxelSpan(0, 1, area);
} else {
// The prioritized area is (in order):
// - the unwalkable area (area=0)
// - the higher valued area
linkedSpans[index] = new LinkedVoxelSpan(0, 1, linkedSpans[index].area == 0 || area == 0 ? 0 : math.max(linkedSpans[index].area, area));
}
}
public void AddLinkedSpan (int index, int bottom, int top, int area, int voxelWalkableClimb, int objectID) {
var minmax = linkedCellMinMax[index];
if (minmax.objectID != objectID) {
linkedCellMinMax[index] = new CellMinMax {
objectID = objectID,
min = bottom,
max = top,
};
} else {
minmax.min = math.min(minmax.min, bottom);
minmax.max = math.max(minmax.max, top);
linkedCellMinMax[index] = minmax;
}
// Clamp to bounding box. If the span was outside the bbox, then bottom will become greater than top.
top = math.min(top, height);
bottom = math.max(bottom, 0);
// Skip span if below or above the bounding box or if the span is zero voxels tall
if (bottom >= top) return;
var utop = (uint)top;
var ubottom = (uint)bottom;
// linkedSpans[index] is the span with the lowest y-coordinate at the position x,z such that index=x+z*width
// i.e linkedSpans is a 2D array laid out in a 1D array (for performance and simplicity)
// Check if there is a root span, otherwise we can just add a new (valid) span and exit
if (linkedSpans[index].bottom == InvalidSpanValue) {
linkedSpans[index] = new LinkedVoxelSpan(ubottom, utop, area);
return;
}
int prev = -1;
// Original index, the first span we visited
int oindex = index;
while (index != -1) {
var current = linkedSpans[index];
if (current.bottom > utop) {
// If the current span's bottom higher up than the span we want to insert's top, then they do not intersect
// and we should just insert a new span here
break;
} else if (current.top < ubottom) {
// The current span and the span we want to insert do not intersect
// so just skip to the next span (it might intersect)
prev = index;
index = current.next;
} else {
// Intersection! Merge the spans
// If two spans have almost the same upper y coordinate then
// we don't just pick the area from the topmost span.
// Instead we pick the maximum of the two areas.
// This ensures that unwalkable spans that end up at the same y coordinate
// as a walkable span (very common for vertical surfaces that meet a walkable surface at a ledge)
// do not end up making the surface unwalkable.
// This is also important for larger distances when there are very small obstacles on the ground.
// For example if a small rock happened to have a surface that was greater than the max slope angle,
// then its surface would be unwalkable. Without this check, even if the rock was tiny, it would
// create a hole in the navmesh.
// voxelWalkableClimb is flagMergeDistance, when a walkable flag is favored before an unwalkable one
// So if a walkable span intersects an unwalkable span, the walkable span can be up to voxelWalkableClimb
// below the unwalkable span and the merged span will still be walkable.
// If both spans are walkable we use the area from the topmost span.
if (math.abs((int)utop - (int)current.top) < voxelWalkableClimb && (area == CompactVoxelField.UnwalkableArea || current.area == CompactVoxelField.UnwalkableArea)) {
// linkedSpans[index] is the lowest span, but we might use that span's area anyway if it is walkable
area = math.max(area, current.area);
} else {
// Pick the area from the topmost span
if (utop < current.top) area = current.area;
}
// Find the new bottom and top for the merged span
ubottom = math.min(current.bottom, ubottom);
utop = math.max(current.top, utop);
// Find the next span in the linked list
int next = current.next;
if (prev != -1) {
// There is a previous span
// Remove this span from the linked list
// TODO: Kinda slow. Check what asm is generated.
var p = linkedSpans[prev];
p.next = next;
linkedSpans[prev] = p;
// Add this span index to a list for recycling
PushToSpanRemovedStack(index);
// Move to the next span in the list
index = next;
} else if (next != -1) {
// This was the root span and there is a span left in the linked list
// Remove this span from the linked list by assigning the next span as the root span
linkedSpans[oindex] = linkedSpans[next];
// Recycle the old span index
PushToSpanRemovedStack(next);
// Move to the next span in the list
// NOP since we just removed the current span, the next span
// we want to visit will have the same index as we are on now (i.e oindex)
} else {
// This was the root span and there are no other spans in the linked list
// Just replace the root span with the merged span and exit
linkedSpans[oindex] = new LinkedVoxelSpan(ubottom, utop, area);
return;
}
}
}
// We now have a merged span that needs to be inserted
// and connected with the existing spans
// The new merged span will be inserted right after 'prev' (if it exists, otherwise before index)
// Take a node from the recycling stack if possible
// Otherwise create a new node (well, just a new index really)
int nextIndex;
if (removedStack.Length > 0) {
// Pop
nextIndex = removedStack[removedStack.Length - 1];
removedStack.RemoveAtSwapBack(removedStack.Length - 1);
} else {
nextIndex = linkedSpans.Length;
linkedSpans.Resize(linkedSpans.Length + 1, NativeArrayOptions.UninitializedMemory);
}
if (prev != -1) {
linkedSpans[nextIndex] = new LinkedVoxelSpan(ubottom, utop, area, linkedSpans[prev].next);
// TODO: Check asm
var p = linkedSpans[prev];
p.next = nextIndex;
linkedSpans[prev] = p;
} else {
linkedSpans[nextIndex] = linkedSpans[oindex];
linkedSpans[oindex] = new LinkedVoxelSpan(ubottom, utop, area, nextIndex);
}
}
}
public struct LinkedVoxelSpan {
public uint bottom;
public uint top;
public int next;
/*Area
* 0 is an unwalkable span (triangle face down)
* 1 is a walkable span (triangle face up)
*/
public int area;
public LinkedVoxelSpan (uint bottom, uint top, int area) {
this.bottom = bottom; this.top = top; this.area = area; this.next = -1;
}
public LinkedVoxelSpan (uint bottom, uint top, int area, int next) {
this.bottom = bottom; this.top = top; this.area = area; this.next = next;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6e41a3dcfac38cd8910584fc5de0d39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,711 @@
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
using Pathfinding.Util;
using Pathfinding.Collections;
namespace Pathfinding.Graphs.Navmesh.Voxelization.Burst {
/// <summary>VoxelContour used for recast graphs.</summary>
public struct VoxelContour {
public int nverts;
/// <summary>Vertex coordinates, each vertex contains 4 components.</summary>
public int vertexStartIndex;
/// <summary>Region ID of the contour</summary>
public int reg;
/// <summary>Area ID of the contour.</summary>
public int area;
}
[BurstCompile(CompileSynchronously = true)]
public struct JobBuildContours : IJob {
public CompactVoxelField field;
public float maxError;
public float maxEdgeLength;
public int buildFlags;
public float cellSize;
public NativeList<VoxelContour> outputContours;
public NativeList<int> outputVerts;
public void Execute () {
outputContours.Clear();
outputVerts.Clear();
int w = field.width;
int d = field.depth;
int wd = w*d;
const ushort BorderReg = VoxelUtilityBurst.BorderReg;
// NOTE: This array may contain uninitialized data, but since we explicitly set all data in it before we use it, it's OK.
var flags = new NativeArray<ushort>(field.spans.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
// Mark boundaries. (@?)
for (int z = 0; z < wd; z += field.width) {
for (int x = 0; x < field.width; x++) {
CompactVoxelCell c = field.cells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
ushort res = 0;
CompactVoxelSpan s = field.spans[i];
if (s.reg == 0 || (s.reg & BorderReg) == BorderReg) {
flags[i] = 0;
continue;
}
for (int dir = 0; dir < 4; dir++) {
int r = 0;
if (s.GetConnection(dir) != CompactVoxelField.NotConnected) {
int ni = field.cells[field.GetNeighbourIndex(x+z, dir)].index + s.GetConnection(dir);
r = field.spans[ni].reg;
}
//@TODO - Why isn't this inside the previous IF
if (r == s.reg) {
res |= (ushort)(1 << dir);
}
}
//Inverse, mark non connected edges.
flags[i] = (ushort)(res ^ 0xf);
}
}
}
NativeList<int> verts = new NativeList<int>(256, Allocator.Temp);
NativeList<int> simplified = new NativeList<int>(64, Allocator.Temp);
for (int z = 0; z < wd; z += field.width) {
for (int x = 0; x < field.width; x++) {
CompactVoxelCell c = field.cells[x+z];
for (int i = c.index, ci = c.index+c.count; i < ci; i++) {
if (flags[i] == 0 || flags[i] == 0xf) {
flags[i] = 0;
continue;
}
int reg = field.spans[i].reg;
if (reg == 0 || (reg & BorderReg) == BorderReg) {
continue;
}
int area = field.areaTypes[i];
verts.Clear();
simplified.Clear();
WalkContour(x, z, i, flags, verts);
SimplifyContour(verts, simplified, maxError, buildFlags);
RemoveDegenerateSegments(simplified);
VoxelContour contour = new VoxelContour {
vertexStartIndex = outputVerts.Length,
nverts = simplified.Length/4,
reg = reg,
area = area,
};
outputVerts.AddRange(simplified.AsArray());
outputContours.Add(contour);
}
}
}
verts.Dispose();
simplified.Dispose();
// Check and merge droppings.
// Sometimes the previous algorithms can fail and create several outputContours
// per area. This pass will try to merge the holes into the main region.
for (int i = 0; i < outputContours.Length; i++) {
VoxelContour cont = outputContours[i];
// Check if the contour is would backwards.
var outputVertsArr = outputVerts.AsArray();
if (CalcAreaOfPolygon2D(outputVertsArr, cont.vertexStartIndex, cont.nverts) < 0) {
// Find another contour which has the same region ID.
int mergeIdx = -1;
for (int j = 0; j < outputContours.Length; j++) {
if (i == j) continue;
if (outputContours[j].nverts > 0 && outputContours[j].reg == cont.reg) {
// Make sure the polygon is correctly oriented.
if (CalcAreaOfPolygon2D(outputVertsArr, outputContours[j].vertexStartIndex, outputContours[j].nverts) > 0) {
mergeIdx = j;
break;
}
}
}
if (mergeIdx == -1) {
// Debug.LogError("rcBuildContours: Could not find merge target for bad contour "+i+".");
} else {
// Debugging
// Debug.LogWarning ("Fixing contour");
VoxelContour mcont = outputContours[mergeIdx];
// Merge by closest points.
GetClosestIndices(outputVertsArr, mcont.vertexStartIndex, mcont.nverts, cont.vertexStartIndex, cont.nverts, out var ia, out var ib);
if (ia == -1 || ib == -1) {
// Debug.LogWarning("rcBuildContours: Failed to find merge points for "+i+" and "+mergeIdx+".");
continue;
}
if (!MergeContours(outputVerts, ref mcont, ref cont, ia, ib)) {
//Debug.LogWarning("rcBuildContours: Failed to merge contours "+i+" and "+mergeIdx+".");
continue;
}
outputContours[mergeIdx] = mcont;
outputContours[i] = cont;
}
}
}
}
void GetClosestIndices (NativeArray<int> verts, int vertexStartIndexA, int nvertsa,
int vertexStartIndexB, int nvertsb,
out int ia, out int ib) {
int closestDist = 0xfffffff;
ia = -1;
ib = -1;
for (int i = 0; i < nvertsa; i++) {
//in is a keyword in C#, so I can't use that as a variable name
int in2 = (i+1) % nvertsa;
int ip = (i+nvertsa-1) % nvertsa;
int va = vertexStartIndexA + i*4;
int van = vertexStartIndexA + in2*4;
int vap = vertexStartIndexA + ip*4;
for (int j = 0; j < nvertsb; ++j) {
int vb = vertexStartIndexB + j*4;
// vb must be "infront" of va.
if (Ileft(verts, vap, va, vb) && Ileft(verts, va, van, vb)) {
int dx = verts[vb+0] - verts[va+0];
int dz = (verts[vb+2]/field.width) - (verts[va+2]/field.width);
int d = dx*dx + dz*dz;
if (d < closestDist) {
ia = i;
ib = j;
closestDist = d;
}
}
}
}
}
public static bool MergeContours (NativeList<int> verts, ref VoxelContour ca, ref VoxelContour cb, int ia, int ib) {
// Note: this will essentially leave junk data in the verts array where the contours were previously.
// This shouldn't be a big problem because MergeContours is normally not called for that many contours (usually none).
int nv = 0;
var startIndex = verts.Length;
// Copy contour A.
for (int i = 0; i <= ca.nverts; i++) {
int src = ca.vertexStartIndex + ((ia+i) % ca.nverts)*4;
verts.Add(verts[src+0]);
verts.Add(verts[src+1]);
verts.Add(verts[src+2]);
verts.Add(verts[src+3]);
nv++;
}
// Copy contour B
for (int i = 0; i <= cb.nverts; i++) {
int src = cb.vertexStartIndex + ((ib+i) % cb.nverts)*4;
verts.Add(verts[src+0]);
verts.Add(verts[src+1]);
verts.Add(verts[src+2]);
verts.Add(verts[src+3]);
nv++;
}
ca.vertexStartIndex = startIndex;
ca.nverts = nv;
cb.vertexStartIndex = 0;
cb.nverts = 0;
return true;
}
public void SimplifyContour (NativeList<int> verts, NativeList<int> simplified, float maxError, int buildFlags) {
// Add initial points.
bool hasConnections = false;
for (int i = 0; i < verts.Length; i += 4) {
if ((verts[i+3] & VoxelUtilityBurst.ContourRegMask) != 0) {
hasConnections = true;
break;
}
}
if (hasConnections) {
// The contour has some portals to other regions.
// Add a new point to every location where the region changes.
for (int i = 0, ni = verts.Length/4; i < ni; i++) {
int ii = (i+1) % ni;
bool differentRegs = (verts[i*4+3] & VoxelUtilityBurst.ContourRegMask) != (verts[ii*4+3] & VoxelUtilityBurst.ContourRegMask);
bool areaBorders = (verts[i*4+3] & VoxelUtilityBurst.RC_AREA_BORDER) != (verts[ii*4+3] & VoxelUtilityBurst.RC_AREA_BORDER);
if (differentRegs || areaBorders) {
simplified.Add(verts[i*4+0]);
simplified.Add(verts[i*4+1]);
simplified.Add(verts[i*4+2]);
simplified.Add(i);
}
}
}
if (simplified.Length == 0) {
// If there is no connections at all,
// create some initial points for the simplification process.
// Find lower-left and upper-right vertices of the contour.
int llx = verts[0];
int lly = verts[1];
int llz = verts[2];
int lli = 0;
int urx = verts[0];
int ury = verts[1];
int urz = verts[2];
int uri = 0;
for (int i = 0; i < verts.Length; i += 4) {
int x = verts[i+0];
int y = verts[i+1];
int z = verts[i+2];
if (x < llx || (x == llx && z < llz)) {
llx = x;
lly = y;
llz = z;
lli = i/4;
}
if (x > urx || (x == urx && z > urz)) {
urx = x;
ury = y;
urz = z;
uri = i/4;
}
}
simplified.Add(llx);
simplified.Add(lly);
simplified.Add(llz);
simplified.Add(lli);
simplified.Add(urx);
simplified.Add(ury);
simplified.Add(urz);
simplified.Add(uri);
}
// Add points until all raw points are within
// error tolerance to the simplified shape.
// This uses the Douglas-Peucker algorithm.
int pn = verts.Length/4;
//Use the max squared error instead
maxError *= maxError;
for (int i = 0; i < simplified.Length/4;) {
int ii = (i+1) % (simplified.Length/4);
int ax = simplified[i*4+0];
int ay = simplified[i*4+1];
int az = simplified[i*4+2];
int ai = simplified[i*4+3];
int bx = simplified[ii*4+0];
int by = simplified[ii*4+1];
int bz = simplified[ii*4+2];
int bi = simplified[ii*4+3];
// Find maximum deviation from the segment.
float maxd = 0;
int maxi = -1;
int ci, cinc, endi;
// Traverse the segment in lexilogical order so that the
// max deviation is calculated similarly when traversing
// opposite segments.
if (bx > ax || (bx == ax && bz > az)) {
cinc = 1;
ci = (ai+cinc) % pn;
endi = bi;
} else {
cinc = pn-1;
ci = (bi+cinc) % pn;
endi = ai;
Memory.Swap(ref ax, ref bx);
Memory.Swap(ref az, ref bz);
}
// Tessellate only outer edges or edges between areas.
if ((verts[ci*4+3] & VoxelUtilityBurst.ContourRegMask) == 0 ||
(verts[ci*4+3] & VoxelUtilityBurst.RC_AREA_BORDER) == VoxelUtilityBurst.RC_AREA_BORDER) {
while (ci != endi) {
float d2 = VectorMath.SqrDistancePointSegmentApproximate(verts[ci*4+0], verts[ci*4+2]/field.width, ax, az/field.width, bx, bz/field.width);
if (d2 > maxd) {
maxd = d2;
maxi = ci;
}
ci = (ci+cinc) % pn;
}
}
// If the max deviation is larger than accepted error,
// add new point, else continue to next segment.
if (maxi != -1 && maxd > maxError) {
// Add space for the new point.
simplified.ResizeUninitialized(simplified.Length + 4);
// Move all points after this one, to leave space to insert the new point
simplified.AsUnsafeSpan().Move((i+1)*4, (i+2)*4, simplified.Length-(i+2)*4);
// Add the point.
simplified[(i+1)*4+0] = verts[maxi*4+0];
simplified[(i+1)*4+1] = verts[maxi*4+1];
simplified[(i+1)*4+2] = verts[maxi*4+2];
simplified[(i+1)*4+3] = maxi;
} else {
i++;
}
}
// Split too long edges
float maxEdgeLen = maxEdgeLength / cellSize;
if (maxEdgeLen > 0 && (buildFlags & (VoxelUtilityBurst.RC_CONTOUR_TESS_WALL_EDGES|VoxelUtilityBurst.RC_CONTOUR_TESS_AREA_EDGES|VoxelUtilityBurst.RC_CONTOUR_TESS_TILE_EDGES)) != 0) {
for (int i = 0; i < simplified.Length/4;) {
if (simplified.Length/4 > 200) {
break;
}
int ii = (i+1) % (simplified.Length/4);
int ax = simplified[i*4+0];
int az = simplified[i*4+2];
int ai = simplified[i*4+3];
int bx = simplified[ii*4+0];
int bz = simplified[ii*4+2];
int bi = simplified[ii*4+3];
// Find maximum deviation from the segment.
int maxi = -1;
int ci = (ai+1) % pn;
// Tessellate only outer edges or edges between areas.
bool tess = false;
// Wall edges.
if ((buildFlags & VoxelUtilityBurst.RC_CONTOUR_TESS_WALL_EDGES) != 0 && (verts[ci*4+3] & VoxelUtilityBurst.ContourRegMask) == 0)
tess = true;
// Edges between areas.
if ((buildFlags & VoxelUtilityBurst.RC_CONTOUR_TESS_AREA_EDGES) != 0 && (verts[ci*4+3] & VoxelUtilityBurst.RC_AREA_BORDER) == VoxelUtilityBurst.RC_AREA_BORDER)
tess = true;
// Border of tile
if ((buildFlags & VoxelUtilityBurst.RC_CONTOUR_TESS_TILE_EDGES) != 0 && (verts[ci*4+3] & VoxelUtilityBurst.BorderReg) == VoxelUtilityBurst.BorderReg)
tess = true;
if (tess) {
int dx = bx - ax;
int dz = (bz/field.width) - (az/field.width);
if (dx*dx + dz*dz > maxEdgeLen*maxEdgeLen) {
// Round based on the segments in lexilogical order so that the
// max tesselation is consistent regardles in which direction
// segments are traversed.
int n = bi < ai ? (bi+pn - ai) : (bi - ai);
if (n > 1) {
if (bx > ax || (bx == ax && bz > az)) {
maxi = (ai + n/2) % pn;
} else {
maxi = (ai + (n+1)/2) % pn;
}
}
}
}
// If the max deviation is larger than accepted error,
// add new point, else continue to next segment.
if (maxi != -1) {
// Add space for the new point.
//simplified.resize(simplified.size()+4);
simplified.Resize(simplified.Length + 4, NativeArrayOptions.UninitializedMemory);
simplified.AsUnsafeSpan().Move((i+1)*4, (i+2)*4, simplified.Length-(i+2)*4);
// Add the point.
simplified[(i+1)*4+0] = verts[maxi*4+0];
simplified[(i+1)*4+1] = verts[maxi*4+1];
simplified[(i+1)*4+2] = verts[maxi*4+2];
simplified[(i+1)*4+3] = maxi;
} else {
++i;
}
}
}
for (int i = 0; i < simplified.Length/4; i++) {
// The edge vertex flag is take from the current raw point,
// and the neighbour region is take from the next raw point.
int ai = (simplified[i*4+3]+1) % pn;
int bi = simplified[i*4+3];
simplified[i*4+3] = (verts[ai*4+3] & VoxelUtilityBurst.ContourRegMask) | (verts[bi*4+3] & VoxelUtilityBurst.RC_BORDER_VERTEX);
}
}
public void WalkContour (int x, int z, int i, NativeArray<ushort> flags, NativeList<int> verts) {
// Choose the first non-connected edge
int dir = 0;
while ((flags[i] & (ushort)(1 << dir)) == 0) {
dir++;
}
int startDir = dir;
int startI = i;
int area = field.areaTypes[i];
int iter = 0;
while (iter++ < 40000) {
// Are we facing a region edge
if ((flags[i] & (ushort)(1 << dir)) != 0) {
// Choose the edge corner
bool isBorderVertex = false;
bool isAreaBorder = false;
int px = x;
int py = GetCornerHeight(x, z, i, dir, ref isBorderVertex);
int pz = z;
// Offset the vertex to land on the corner of the span.
// The resulting coordinates have an implicit 1/2 voxel offset because all corners
// are in the middle between two adjacent integer voxel coordinates.
switch (dir) {
case 0: pz += field.width; break;
case 1: px++; pz += field.width; break;
case 2: px++; break;
}
int r = 0;
CompactVoxelSpan s = field.spans[i];
if (s.GetConnection(dir) != CompactVoxelField.NotConnected) {
int ni = (int)field.cells[field.GetNeighbourIndex(x+z, dir)].index + s.GetConnection(dir);
r = (int)field.spans[ni].reg;
if (area != field.areaTypes[ni]) {
isAreaBorder = true;
}
}
if (isBorderVertex) {
r |= VoxelUtilityBurst.RC_BORDER_VERTEX;
}
if (isAreaBorder) {
r |= VoxelUtilityBurst.RC_AREA_BORDER;
}
verts.Add(px);
verts.Add(py);
verts.Add(pz);
verts.Add(r);
flags[i] = (ushort)(flags[i] & ~(1 << dir)); // Remove visited edges
// & 0x3 is the same as % 4 (for positive numbers)
dir = (dir+1) & 0x3; // Rotate CW
} else {
int ni = -1;
int nx = x + VoxelUtilityBurst.DX[dir];
int nz = z + VoxelUtilityBurst.DZ[dir]*field.width;
CompactVoxelSpan s = field.spans[i];
if (s.GetConnection(dir) != CompactVoxelField.NotConnected) {
CompactVoxelCell nc = field.cells[nx+nz];
ni = (int)nc.index + s.GetConnection(dir);
}
if (ni == -1) {
Debug.LogWarning("Degenerate triangles might have been generated.\n" +
"Usually this is not a problem, but if you have a static level, try to modify the graph settings slightly to avoid this edge case.");
return;
}
x = nx;
z = nz;
i = ni;
// & 0x3 is the same as % 4 (modulo 4)
dir = (dir+3) & 0x3; // Rotate CCW
}
if (startI == i && startDir == dir) {
break;
}
}
}
public int GetCornerHeight (int x, int z, int i, int dir, ref bool isBorderVertex) {
CompactVoxelSpan s = field.spans[i];
int cornerHeight = (int)s.y;
// dir + 1 step in the clockwise direction
int dirp = (dir+1) & 0x3;
unsafe {
// We need a small buffer to hold regions for each axis aligned neighbour.
// This requires unsafe, though. In future C# versions we can use Span<T>.
//
// dir
// X---->
// dirp |
// v
//
//
// The regs array will contain the regions for the following spans,
// where the 0th span is the current span.
// 'x' signifies the position of the corner we are interested in.
// This is the shared vertex corner the four spans.
// It is conceptually at the current span's position + 0.5*dir + 0.5*dirp
//
//
// 0 --------- 1 -> dir
// | |
// | x |
// | |
// 3 --------- 2
//
// | dirp
// v
//
var regs = stackalloc uint[] { 0, 0, 0, 0 };
regs[0] = (uint)field.spans[i].reg | ((uint)field.areaTypes[i] << 16);
if (s.GetConnection(dir) != CompactVoxelField.NotConnected) {
int neighbourCell = field.GetNeighbourIndex(x+z, dir);
int ni = (int)field.cells[neighbourCell].index + s.GetConnection(dir);
CompactVoxelSpan ns = field.spans[ni];
cornerHeight = System.Math.Max(cornerHeight, (int)ns.y);
regs[1] = (uint)ns.reg | ((uint)field.areaTypes[ni] << 16);
if (ns.GetConnection(dirp) != CompactVoxelField.NotConnected) {
int neighbourCell2 = field.GetNeighbourIndex(neighbourCell, dirp);
int ni2 = (int)field.cells[neighbourCell2].index + ns.GetConnection(dirp);
CompactVoxelSpan ns2 = field.spans[ni2];
cornerHeight = System.Math.Max(cornerHeight, (int)ns2.y);
regs[2] = (uint)ns2.reg | ((uint)field.areaTypes[ni2] << 16);
}
}
if (s.GetConnection(dirp) != CompactVoxelField.NotConnected) {
int neighbourCell = field.GetNeighbourIndex(x+z, dirp);
int ni = (int)field.cells[neighbourCell].index + s.GetConnection(dirp);
CompactVoxelSpan ns = field.spans[ni];
cornerHeight = System.Math.Max(cornerHeight, (int)ns.y);
regs[3] = (uint)ns.reg | ((uint)field.areaTypes[ni] << 16);
if (ns.GetConnection(dir) != CompactVoxelField.NotConnected) {
int neighbourCell2 = field.GetNeighbourIndex(neighbourCell, dir);
int ni2 = (int)field.cells[neighbourCell2].index + ns.GetConnection(dir);
CompactVoxelSpan ns2 = field.spans[ni2];
cornerHeight = System.Math.Max(cornerHeight, (int)ns2.y);
regs[2] = (uint)ns2.reg | ((uint)field.areaTypes[ni2] << 16);
}
}
// Zeroes show up when there are no connections to some spans. E.g. if the current span is on a ledge.
bool noZeros = regs[0] != 0 && regs[1] != 0 && regs[2] != 0 && regs[3] != 0;
// Check if the vertex is special edge vertex, these vertices will be removed later.
for (int j = 0; j < 4; ++j) {
int a = j;
int b = (j+1) & 0x3;
int c = (j+2) & 0x3;
int d = (j+3) & 0x3;
// The vertex is a border vertex there are two same exterior cells in a row,
// followed by two interior cells and none of the regions are out of bounds.
bool twoSameExts = (regs[a] & regs[b] & VoxelUtilityBurst.BorderReg) != 0 && regs[a] == regs[b];
bool twoInts = ((regs[c] | regs[d]) & VoxelUtilityBurst.BorderReg) == 0;
bool intsSameArea = (regs[c]>>16) == (regs[d]>>16);
if (twoSameExts && twoInts && intsSameArea && noZeros) {
isBorderVertex = true;
break;
}
}
}
return cornerHeight;
}
static void RemoveRange (NativeList<int> arr, int index, int count) {
for (int i = index; i < arr.Length - count; i++) {
arr[i] = arr[i+count];
}
arr.Resize(arr.Length - count, NativeArrayOptions.UninitializedMemory);
}
static void RemoveDegenerateSegments (NativeList<int> simplified) {
// Remove adjacent vertices which are equal on xz-plane,
// or else the triangulator will get confused
for (int i = 0; i < simplified.Length/4; i++) {
int ni = i+1;
if (ni >= (simplified.Length/4))
ni = 0;
if (simplified[i*4+0] == simplified[ni*4+0] &&
simplified[i*4+2] == simplified[ni*4+2]) {
// Degenerate segment, remove.
RemoveRange(simplified, i, 4);
}
}
}
int CalcAreaOfPolygon2D (NativeArray<int> verts, int vertexStartIndex, int nverts) {
int area = 0;
for (int i = 0, j = nverts-1; i < nverts; j = i++) {
int vi = vertexStartIndex + i*4;
int vj = vertexStartIndex + j*4;
area += verts[vi+0] * (verts[vj+2]/field.width) - verts[vj+0] * (verts[vi+2]/field.width);
}
return (area+1) / 2;
}
static bool Ileft (NativeArray<int> verts, int a, int b, int c) {
return (verts[b+0] - verts[a+0]) * (verts[c+2] - verts[a+2]) - (verts[c+0] - verts[a+0]) * (verts[b+2] - verts[a+2]) <= 0;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aba1f429a9dee0ef98d35221ff450cda
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,551 @@
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
namespace Pathfinding.Graphs.Navmesh.Voxelization.Burst {
using System;
using Pathfinding.Jobs;
using Pathfinding.Collections;
#if MODULE_COLLECTIONS_2_1_0_OR_NEWER
using NativeHashMapInt3Int = Unity.Collections.NativeHashMap<Int3, int>;
#else
using NativeHashMapInt3Int = Unity.Collections.NativeParallelHashMap<Int3, int>;
#endif
/// <summary>VoxelMesh used for recast graphs.</summary>
public struct VoxelMesh : IArenaDisposable {
/// <summary>Vertices of the mesh</summary>
public NativeList<Int3> verts;
/// <summary>
/// Triangles of the mesh.
/// Each element points to a vertex in the <see cref="verts"/> array
/// </summary>
public NativeList<int> tris;
/// <summary>Area index for each triangle</summary>
public NativeList<int> areas;
void IArenaDisposable.DisposeWith (DisposeArena arena) {
arena.Add(verts);
arena.Add(tris);
arena.Add(areas);
}
}
/// <summary>Builds a polygon mesh from a contour set.</summary>
[BurstCompile]
public struct JobBuildMesh : IJob {
public NativeList<int> contourVertices;
/// <summary>contour set to build a mesh from.</summary>
public NativeList<VoxelContour> contours;
/// <summary>Results will be written to this mesh.</summary>
public VoxelMesh mesh;
public CompactVoxelField field;
/// <summary>
/// Returns T iff (v_i, v_j) is a proper internal
/// diagonal of P.
/// </summary>
static bool Diagonal (int i, int j, int n, NativeArray<int> verts, NativeArray<int> indices) {
return InCone(i, j, n, verts, indices) && Diagonalie(i, j, n, verts, indices);
}
static bool InCone (int i, int j, int n, NativeArray<int> verts, NativeArray<int> indices) {
int pi = (indices[i] & 0x0fffffff) * 3;
int pj = (indices[j] & 0x0fffffff) * 3;
int pi1 = (indices[Next(i, n)] & 0x0fffffff) * 3;
int pin1 = (indices[Prev(i, n)] & 0x0fffffff) * 3;
// If P[i] is a convex vertex [ i+1 left or on (i-1,i) ].
if (LeftOn(pin1, pi, pi1, verts))
return Left(pi, pj, pin1, verts) && Left(pj, pi, pi1, verts);
// Assume (i-1,i,i+1) not collinear.
// else P[i] is reflex.
return !(LeftOn(pi, pj, pi1, verts) && LeftOn(pj, pi, pin1, verts));
}
/// <summary>
/// Returns true iff c is strictly to the left of the directed
/// line through a to b.
/// </summary>
static bool Left (int a, int b, int c, NativeArray<int> verts) {
return Area2(a, b, c, verts) < 0;
}
static bool LeftOn (int a, int b, int c, NativeArray<int> verts) {
return Area2(a, b, c, verts) <= 0;
}
static bool Collinear (int a, int b, int c, NativeArray<int> verts) {
return Area2(a, b, c, verts) == 0;
}
public static int Area2 (int a, int b, int c, NativeArray<int> verts) {
return (verts[b] - verts[a]) * (verts[c+2] - verts[a+2]) - (verts[c+0] - verts[a+0]) * (verts[b+2] - verts[a+2]);
}
/// <summary>
/// Returns T iff (v_i, v_j) is a proper internal *or* external
/// diagonal of P, *ignoring edges incident to v_i and v_j*.
/// </summary>
static bool Diagonalie (int i, int j, int n, NativeArray<int> verts, NativeArray<int> indices) {
int d0 = (indices[i] & 0x0fffffff) * 3;
int d1 = (indices[j] & 0x0fffffff) * 3;
/*int a = (i+1) % indices.Length;
* if (a == j) a = (i-1 + indices.Length) % indices.Length;
* int a_v = (indices[a] & 0x0fffffff) * 4;
*
* if (a != j && Collinear (d0,a_v,d1,verts)) {
* return false;
* }*/
// For each edge (k,k+1) of P
for (int k = 0; k < n; k++) {
int k1 = Next(k, n);
// Skip edges incident to i or j
if (!((k == i) || (k1 == i) || (k == j) || (k1 == j))) {
int p0 = (indices[k] & 0x0fffffff) * 3;
int p1 = (indices[k1] & 0x0fffffff) * 3;
if (Vequal(d0, p0, verts) || Vequal(d1, p0, verts) || Vequal(d0, p1, verts) || Vequal(d1, p1, verts))
continue;
if (Intersect(d0, d1, p0, p1, verts))
return false;
}
}
return true;
}
// Exclusive or: true iff exactly one argument is true.
// The arguments are negated to ensure that they are 0/1
// values. Then the bitwise Xor operator may apply.
// (This idea is due to Michael Baldwin.)
static bool Xorb (bool x, bool y) {
return !x ^ !y;
}
// Returns true iff ab properly intersects cd: they share
// a point interior to both segments. The properness of the
// intersection is ensured by using strict leftness.
static bool IntersectProp (int a, int b, int c, int d, NativeArray<int> verts) {
// Eliminate improper cases.
if (Collinear(a, b, c, verts) || Collinear(a, b, d, verts) ||
Collinear(c, d, a, verts) || Collinear(c, d, b, verts))
return false;
return Xorb(Left(a, b, c, verts), Left(a, b, d, verts)) && Xorb(Left(c, d, a, verts), Left(c, d, b, verts));
}
// Returns T iff (a,b,c) are collinear and point c lies
// on the closed segement ab.
static bool Between (int a, int b, int c, NativeArray<int> verts) {
if (!Collinear(a, b, c, verts))
return false;
// If ab not vertical, check betweenness on x; else on y.
if (verts[a+0] != verts[b+0])
return ((verts[a+0] <= verts[c+0]) && (verts[c+0] <= verts[b+0])) || ((verts[a+0] >= verts[c+0]) && (verts[c+0] >= verts[b+0]));
else
return ((verts[a+2] <= verts[c+2]) && (verts[c+2] <= verts[b+2])) || ((verts[a+2] >= verts[c+2]) && (verts[c+2] >= verts[b+2]));
}
// Returns true iff segments ab and cd intersect, properly or improperly.
static bool Intersect (int a, int b, int c, int d, NativeArray<int> verts) {
if (IntersectProp(a, b, c, d, verts))
return true;
else if (Between(a, b, c, verts) || Between(a, b, d, verts) ||
Between(c, d, a, verts) || Between(c, d, b, verts))
return true;
else
return false;
}
static bool Vequal (int a, int b, NativeArray<int> verts) {
return verts[a+0] == verts[b+0] && verts[a+2] == verts[b+2];
}
/// <summary>(i-1+n) % n assuming 0 <= i < n</summary>
static int Prev (int i, int n) { return i-1 >= 0 ? i-1 : n-1; }
/// <summary>(i+1) % n assuming 0 <= i < n</summary>
static int Next (int i, int n) { return i+1 < n ? i+1 : 0; }
static int AddVertex (NativeList<Int3> vertices, NativeHashMapInt3Int vertexMap, Int3 vertex) {
if (vertexMap.TryGetValue(vertex, out var index)) {
return index;
}
vertices.AddNoResize(vertex);
vertexMap.Add(vertex, vertices.Length-1);
return vertices.Length-1;
}
public void Execute () {
// Maximum allowed vertices per polygon. Currently locked to 3.
var nvp = 3;
int maxVertices = 0;
int maxTris = 0;
int maxVertsPerCont = 0;
for (int i = 0; i < contours.Length; i++) {
// Skip null contours.
if (contours[i].nverts < 3) continue;
maxVertices += contours[i].nverts;
maxTris += contours[i].nverts - 2;
maxVertsPerCont = System.Math.Max(maxVertsPerCont, contours[i].nverts);
}
mesh.verts.Clear();
if (maxVertices > mesh.verts.Capacity) mesh.verts.SetCapacity(maxVertices);
mesh.tris.ResizeUninitialized(maxTris*nvp);
mesh.areas.ResizeUninitialized(maxTris);
var verts = mesh.verts;
var polys = mesh.tris;
var areas = mesh.areas;
var indices = new NativeArray<int>(maxVertsPerCont, Allocator.Temp);
var tris = new NativeArray<int>(maxVertsPerCont*3, Allocator.Temp);
var verticesToRemove = new NativeArray<bool>(maxVertices, Allocator.Temp);
var vertexPointers = new NativeHashMapInt3Int(maxVertices, Allocator.Temp);
int polyIndex = 0;
int areaIndex = 0;
for (int i = 0; i < contours.Length; i++) {
VoxelContour cont = contours[i];
// Skip degenerate contours
if (cont.nverts < 3) {
continue;
}
for (int j = 0; j < cont.nverts; j++) {
// Convert the z coordinate from the form z*voxelArea.width which is used in other places for performance
contourVertices[cont.vertexStartIndex + j*4+2] /= field.width;
}
// Copy the vertex positions
for (int j = 0; j < cont.nverts; j++) {
// Try to remove all border vertices
// See https://digestingduck.blogspot.com/2009/08/navmesh-height-accuracy-pt-5.html
var vertexRegion = contourVertices[cont.vertexStartIndex + j*4+3];
// Add a new vertex, or reuse an existing one if it has already been added to the mesh
var idx = AddVertex(verts, vertexPointers, new Int3(
contourVertices[cont.vertexStartIndex + j*4],
contourVertices[cont.vertexStartIndex + j*4+1],
contourVertices[cont.vertexStartIndex + j*4+2]
));
indices[j] = idx;
verticesToRemove[idx] = (vertexRegion & VoxelUtilityBurst.RC_BORDER_VERTEX) != 0;
}
// Triangulate the contour
int ntris = Triangulate(cont.nverts, verts.AsArray().Reinterpret<int>(12), indices, tris);
if (ntris < 0) {
// Degenerate triangles. This may lead to a hole in the navmesh.
// We add the triangles that the triangulation generated before it failed.
ntris = -ntris;
}
// Copy the resulting triangles to the mesh
for (int j = 0; j < ntris*3; polyIndex++, j++) {
polys[polyIndex] = tris[j];
}
// Mark all triangles generated by this contour
// as having the area cont.area
for (int j = 0; j < ntris; areaIndex++, j++) {
areas[areaIndex] = cont.area;
}
}
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if (areaIndex > mesh.areas.Length) throw new System.Exception("Ended up at an unexpected area index");
if (polyIndex > mesh.tris.Length) throw new System.Exception("Ended up at an unexpected poly index");
#endif
// polyIndex might in rare cases not be equal to mesh.tris.Length.
// This can happen if degenerate triangles were generated.
// So we make sure the list is truncated to the right size here.
mesh.tris.ResizeUninitialized(polyIndex);
// Same thing for area index
mesh.areas.ResizeUninitialized(areaIndex);
RemoveTileBorderVertices(ref mesh, verticesToRemove);
}
void RemoveTileBorderVertices (ref VoxelMesh mesh, NativeArray<bool> verticesToRemove) {
// Iterate in reverse to avoid having to update the verticesToRemove array as we remove vertices
var vertexScratch = new NativeArray<byte>(mesh.verts.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int i = mesh.verts.Length - 1; i >= 0; i--) {
if (verticesToRemove[i] && CanRemoveVertex(ref mesh, i, vertexScratch.AsUnsafeSpan())) {
RemoveVertex(ref mesh, i);
}
}
}
bool CanRemoveVertex (ref VoxelMesh mesh, int vertexToRemove, UnsafeSpan<byte> vertexScratch) {
UnityEngine.Assertions.Assert.IsTrue(vertexScratch.Length >= mesh.verts.Length);
int remainingEdges = 0;
for (int i = 0; i < mesh.tris.Length; i += 3) {
int touched = 0;
for (int j = 0; j < 3; j++) {
if (mesh.tris[i+j] == vertexToRemove) {
// This vertex is used by a triangle
touched++;
}
}
if (touched > 0) {
if (touched > 1) throw new Exception("Degenerate triangle. This should have already been removed.");
// If one vertex is removed from a triangle, 1 edge remains
remainingEdges++;
}
}
if (remainingEdges <= 2) {
// There would be too few edges remaining to create a polygon.
// This can happen for example when a tip of a triangle is marked
// as deletion, but there are no other polys that share the vertex.
// In this case, the vertex should not be removed.
return false;
}
vertexScratch.FillZeros();
for (int i = 0; i < mesh.tris.Length; i += 3) {
for (int a = 0, b = 2; a < 3; b = a++) {
if (mesh.tris[i+a] == vertexToRemove || mesh.tris[i+b] == vertexToRemove) {
// This edge is used by a triangle
int v1 = mesh.tris[i+a];
int v2 = mesh.tris[i+b];
// Update the shared count for the edge.
// We identify the edge by the vertex index which is not the vertex to remove.
vertexScratch[v2 == vertexToRemove ? v1 : v2]++;
}
}
}
int openEdges = 0;
int multiSharedEdges = 0;
for (int i = 0; i < vertexScratch.Length; i++) {
if (vertexScratch[i] == 1) openEdges++;
else if (vertexScratch[i] > 2) multiSharedEdges++;
}
if (multiSharedEdges > 0) {
// This should not happen in valid navmeshes. But if the navmesh for some reason has overlapping triangles due to some other bug,
// we should return false here, as otherwise we might end up in an infinite loop when trying to remove the vertex.
Debug.LogError($"Vertex has multiple shared edges. This should not happen. Navmesh must be corrupt. Trying to not make it worse.");
return false;
}
// There should be no more than 2 open edges.
// This catches the case that two non-adjacent polygons
// share the removed vertex. In that case, do not remove the vertex.
return openEdges <= 2;
}
void RemoveVertex (ref VoxelMesh mesh, int vertexToRemove) {
// Note: Assumes CanRemoveVertex has been called and returned true
var remainingEdges = new NativeList<int>(16, Allocator.Temp);
var area = -1;
// Find all triangles that use this vertex
for (int i = 0; i < mesh.tris.Length; i += 3) {
int touched = -1;
for (int j = 0; j < 3; j++) {
if (mesh.tris[i+j] == vertexToRemove) {
// This vertex is used by a triangle
touched = j;
break;
}
}
if (touched != -1) {
// Note: Only vertices that are not on an area border will be chosen (see GetCornerHeight),
// so it is safe to assume that all triangles that share this vertex also share an area.
area = mesh.areas[i/3];
// If one vertex is removed from a triangle, 1 edge remains
remainingEdges.Add(mesh.tris[i+((touched+1) % 3)]);
remainingEdges.Add(mesh.tris[i+((touched+2) % 3)]);
mesh.tris[i+0] = mesh.tris[mesh.tris.Length-3+0];
mesh.tris[i+1] = mesh.tris[mesh.tris.Length-3+1];
mesh.tris[i+2] = mesh.tris[mesh.tris.Length-3+2];
mesh.tris.Length -= 3;
mesh.areas.RemoveAtSwapBack(i/3);
i -= 3;
}
}
UnityEngine.Assertions.Assert.AreNotEqual(-1, area);
// Build a sorted list of all vertices in the contour for the hole
var sortedVertices = new NativeList<int>(remainingEdges.Length/2 + 1, Allocator.Temp);
sortedVertices.Add(remainingEdges[remainingEdges.Length-2]);
sortedVertices.Add(remainingEdges[remainingEdges.Length-1]);
remainingEdges.Length -= 2;
while (remainingEdges.Length > 0) {
for (int i = remainingEdges.Length - 2; i >= 0; i -= 2) {
var a = remainingEdges[i];
var b = remainingEdges[i+1];
bool added = false;
if (sortedVertices[0] == b) {
sortedVertices.InsertRange(0, 1);
sortedVertices[0] = a;
added = true;
}
if (sortedVertices[sortedVertices.Length-1] == a) {
sortedVertices.AddNoResize(b);
added = true;
}
if (added) {
// Remove the edge and swap with the last one
remainingEdges[i] = remainingEdges[remainingEdges.Length-2];
remainingEdges[i+1] = remainingEdges[remainingEdges.Length-1];
remainingEdges.Length -= 2;
}
}
}
// Remove the vertex
mesh.verts.RemoveAt(vertexToRemove);
// Patch indices to account for the removed vertex
for (int i = 0; i < mesh.tris.Length; i++) {
if (mesh.tris[i] > vertexToRemove) mesh.tris[i]--;
}
for (int i = 0; i < sortedVertices.Length; i++) {
if (sortedVertices[i] > vertexToRemove) sortedVertices[i]--;
}
var maxIndices = (sortedVertices.Length - 2) * 3;
var trisBeforeResize = mesh.tris.Length;
mesh.tris.Length += maxIndices;
int newTriCount = Triangulate(
sortedVertices.Length,
mesh.verts.AsArray().Reinterpret<int>(12),
sortedVertices.AsArray(),
// Insert the new triangles at the end of the array
mesh.tris.AsArray().GetSubArray(trisBeforeResize, maxIndices)
);
if (newTriCount < 0) {
// Degenerate triangles. This may lead to a hole in the navmesh.
// We add the triangles that the triangulation generated before it failed.
newTriCount = -newTriCount;
}
// Resize the triangle array to the correct size
mesh.tris.ResizeUninitialized(trisBeforeResize + newTriCount*3);
mesh.areas.AddReplicate(area, newTriCount);
UnityEngine.Assertions.Assert.AreEqual(mesh.areas.Length, mesh.tris.Length/3);
}
static int Triangulate (int n, NativeArray<int> verts, NativeArray<int> indices, NativeArray<int> tris) {
int ntris = 0;
var dst = tris;
int dstIndex = 0;
// The last bit of the index is used to indicate if the vertex can be removed
// in an ear-cutting operation.
const int CanBeRemovedBit = 0x40000000;
// Used to get only the index value, without any flag bits.
const int IndexMask = 0x0fffffff;
for (int i = 0; i < n; i++) {
int i1 = Next(i, n);
int i2 = Next(i1, n);
if (Diagonal(i, i2, n, verts, indices)) {
indices[i1] |= CanBeRemovedBit;
}
}
while (n > 3) {
int minLen = int.MaxValue;
int mini = -1;
for (int q = 0; q < n; q++) {
int q1 = Next(q, n);
if ((indices[q1] & CanBeRemovedBit) != 0) {
int p0 = (indices[q] & IndexMask) * 3;
int p2 = (indices[Next(q1, n)] & IndexMask) * 3;
int dx = verts[p2+0] - verts[p0+0];
int dz = verts[p2+2] - verts[p0+2];
//Squared distance
int len = dx*dx + dz*dz;
if (len < minLen) {
minLen = len;
mini = q;
}
}
}
if (mini == -1) {
Debug.LogWarning("Degenerate triangles might have been generated.\n" +
"Usually this is not a problem, but if you have a static level, try to modify the graph settings slightly to avoid this edge case.");
return -ntris;
}
int i = mini;
int i1 = Next(i, n);
int i2 = Next(i1, n);
dst[dstIndex] = indices[i] & IndexMask;
dstIndex++;
dst[dstIndex] = indices[i1] & IndexMask;
dstIndex++;
dst[dstIndex] = indices[i2] & IndexMask;
dstIndex++;
ntris++;
// Removes P[i1] by copying P[i+1]...P[n-1] left one index.
n--;
for (int k = i1; k < n; k++) {
indices[k] = indices[k+1];
}
if (i1 >= n) i1 = 0;
i = Prev(i1, n);
// Update diagonal flags.
if (Diagonal(Prev(i, n), i1, n, verts, indices)) {
indices[i] |= CanBeRemovedBit;
} else {
indices[i] &= IndexMask;
}
if (Diagonal(i, Next(i1, n), n, verts, indices)) {
indices[i1] |= CanBeRemovedBit;
} else {
indices[i1] &= IndexMask;
}
}
dst[dstIndex] = indices[0] & IndexMask;
dstIndex++;
dst[dstIndex] = indices[1] & IndexMask;
dstIndex++;
dst[dstIndex] = indices[2] & IndexMask;
dstIndex++;
ntris++;
return ntris;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 73110b746664b5ec197eda5f732356a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,194 @@
using Pathfinding.Util;
using Unity.Burst;
using Pathfinding.Collections;
namespace Pathfinding.Graphs.Navmesh.Voxelization {
/// <summary>Utility for clipping polygons</summary>
internal struct Int3PolygonClipper {
unsafe fixed float clipPolygonCache[7*3];
/// <summary>
/// Clips a polygon against an axis aligned half plane.
///
/// Returns: Number of output vertices
///
/// The vertices will be scaled and then offset, after that they will be cut using either the
/// x axis, y axis or the z axis as the cutting line. The resulting vertices will be added to the
/// vOut array in their original space (i.e before scaling and offsetting).
/// </summary>
/// <param name="vIn">Input vertices</param>
/// <param name="n">Number of input vertices (may be less than the length of the vIn array)</param>
/// <param name="vOut">Output vertices, needs to be large enough</param>
/// <param name="multi">Scale factor for the input vertices</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
/// <param name="axis">Axis to cut along, either x=0, y=1, z=2</param>
public int ClipPolygon (UnsafeSpan<Int3> vIn, int n, UnsafeSpan<Int3> vOut, int multi, int offset, int axis) {
unsafe {
for (int i = 0; i < n; i++) {
clipPolygonCache[i] = multi*vIn[i][axis]+offset;
}
// Number of resulting vertices
int m = 0;
for (int i = 0, j = n-1; i < n; j = i, i++) {
bool prev = clipPolygonCache[j] >= 0;
bool curr = clipPolygonCache[i] >= 0;
if (prev != curr) {
double s = (double)clipPolygonCache[j] / (clipPolygonCache[j] - clipPolygonCache[i]);
vOut[m] = vIn[j] + (vIn[i]-vIn[j])*s;
m++;
}
if (curr) {
vOut[m] = vIn[i];
m++;
}
}
return m;
}
}
}
/// <summary>Utility for clipping polygons</summary>
internal struct VoxelPolygonClipper {
public unsafe fixed float x[8];
public unsafe fixed float y[8];
public unsafe fixed float z[8];
public int n;
public UnityEngine.Vector3 this[int i] {
set {
unsafe {
x[i] = value.x;
y[i] = value.y;
z[i] = value.z;
}
}
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
/// The polygons stored in this object are clipped against the half plane at x = -offset.
/// </summary>
/// <param name="result">Ouput vertices</param>
/// <param name="multi">Scale factor for the input vertices. Should be +1 or -1. If -1 the negative half plane is kept.</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
public void ClipPolygonAlongX ([NoAlias] ref VoxelPolygonClipper result, float multi, float offset) {
unsafe {
// Number of resulting vertices
int m = 0;
float dj = multi*x[(n-1)]+offset;
for (int i = 0, j = n-1; i < n; j = i, i++) {
float di = multi*x[i]+offset;
bool prev = dj >= 0;
bool curr = di >= 0;
if (prev != curr) {
float s = dj / (dj - di);
result.x[m] = x[j] + (x[i]-x[j])*s;
result.y[m] = y[j] + (y[i]-y[j])*s;
result.z[m] = z[j] + (z[i]-z[j])*s;
m++;
}
if (curr) {
result.x[m] = x[i];
result.y[m] = y[i];
result.z[m] = z[i];
m++;
}
dj = di;
}
result.n = m;
}
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
/// The polygons stored in this object are clipped against the half plane at z = -offset.
/// </summary>
/// <param name="result">Ouput vertices. Only the Y and Z coordinates are calculated. The X coordinates are undefined.</param>
/// <param name="multi">Scale factor for the input vertices. Should be +1 or -1. If -1 the negative half plane is kept.</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
public void ClipPolygonAlongZWithYZ ([NoAlias] ref VoxelPolygonClipper result, float multi, float offset) {
unsafe {
// Number of resulting vertices
int m = 0;
Unity.Burst.CompilerServices.Hint.Assume(n >= 0);
Unity.Burst.CompilerServices.Hint.Assume(n <= 8);
float dj = multi*z[(n-1)]+offset;
for (int i = 0, j = n-1; i < n; j = i, i++) {
float di = multi*z[i]+offset;
bool prev = dj >= 0;
bool curr = di >= 0;
if (prev != curr) {
float s = dj / (dj - di);
result.y[m] = y[j] + (y[i]-y[j])*s;
result.z[m] = z[j] + (z[i]-z[j])*s;
m++;
}
if (curr) {
result.y[m] = y[i];
result.z[m] = z[i];
m++;
}
dj = di;
}
result.n = m;
}
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
/// The polygons stored in this object are clipped against the half plane at z = -offset.
/// </summary>
/// <param name="result">Ouput vertices. Only the Y coordinates are calculated. The X and Z coordinates are undefined.</param>
/// <param name="multi">Scale factor for the input vertices. Should be +1 or -1. If -1 the negative half plane is kept.</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
public void ClipPolygonAlongZWithY ([NoAlias] ref VoxelPolygonClipper result, float multi, float offset) {
unsafe {
// Number of resulting vertices
int m = 0;
Unity.Burst.CompilerServices.Hint.Assume(n >= 3);
Unity.Burst.CompilerServices.Hint.Assume(n <= 8);
float dj = multi*z[n-1]+offset;
for (int i = 0, j = n-1; i < n; j = i, i++) {
float di = multi*z[i]+offset;
bool prev = dj >= 0;
bool curr = di >= 0;
if (prev != curr) {
float s = dj / (dj - di);
result.y[m] = y[j] + (y[i]-y[j])*s;
m++;
}
if (curr) {
result.y[m] = y[i];
m++;
}
dj = di;
}
result.n = m;
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 10347e1eaceee428fa14386ccbaffde5
timeCreated: 1454161567
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,485 @@
using UnityEngine;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Burst;
namespace Pathfinding.Graphs.Navmesh.Voxelization.Burst {
using Pathfinding.Util;
using Unity.Collections.LowLevel.Unsafe;
using Pathfinding.Collections;
public struct RasterizationMesh {
public UnsafeSpan<float3> vertices;
public UnsafeSpan<int> triangles;
public int area;
/// <summary>World bounds of the mesh. Assumed to already be multiplied with the matrix</summary>
public Bounds bounds;
public Matrix4x4 matrix;
/// <summary>
/// If true then the mesh will be treated as solid and its interior will be unwalkable.
/// The unwalkable region will be the minimum to maximum y coordinate in each cell.
/// </summary>
public bool solid;
/// <summary>If true, both sides of the mesh will be walkable. If false, only the side that the normal points towards will be walkable</summary>
public bool doubleSided;
/// <summary>If true, the <see cref="area"/> will be interpreted as a node tag and applied to the final nodes</summary>
public bool areaIsTag;
/// <summary>
/// If true, the mesh will be flattened to the base of the graph during rasterization.
///
/// This is intended for rasterizing 2D meshes which always lie in a single plane.
///
/// This will also cause unwalkable spans have precedence over walkable ones at all times, instead of
/// only when the unwalkable span is sufficiently high up over a walkable span. Since when flattening,
/// "sufficiently high up" makes no sense.
/// </summary>
public bool flatten;
}
[BurstCompile(CompileSynchronously = true)]
public struct JobVoxelize : IJob {
[ReadOnly]
public NativeArray<RasterizationMesh> inputMeshes;
[ReadOnly]
public NativeArray<int> bucket;
/// <summary>Maximum ledge height that is considered to still be traversable. [Limit: >=0] [Units: vx]</summary>
public int voxelWalkableClimb;
/// <summary>
/// Minimum floor to 'ceiling' height that will still allow the floor area to
/// be considered walkable. [Limit: >= 3] [Units: vx]
/// </summary>
public uint voxelWalkableHeight;
/// <summary>The xz-plane cell size to use for fields. [Limit: > 0] [Units: wu]</summary>
public float cellSize;
/// <summary>The y-axis cell size to use for fields. [Limit: > 0] [Units: wu]</summary>
public float cellHeight;
/// <summary>The maximum slope that is considered walkable. [Limits: 0 <= value < 90] [Units: Degrees]</summary>
public float maxSlope;
public Matrix4x4 graphTransform;
public Bounds graphSpaceBounds;
public Vector2 graphSpaceLimits;
public LinkedVoxelField voxelArea;
public void Execute () {
// Transform from voxel space to graph space.
// then scale from voxel space (one unit equals one voxel)
// Finally add min
Matrix4x4 voxelMatrix = Matrix4x4.TRS(graphSpaceBounds.min, Quaternion.identity, Vector3.one) * Matrix4x4.Scale(new Vector3(cellSize, cellHeight, cellSize));
// Transform from voxel space to world space
// add half a voxel to fix rounding
var transform = graphTransform * voxelMatrix * Matrix4x4.Translate(new Vector3(0.5f, 0, 0.5f));
var world2voxelMatrix = transform.inverse;
// Cosine of the slope limit in voxel space (some tweaks are needed because the voxel space might be stretched out along the y axis)
float slopeLimit = math.cos(math.atan((cellSize/cellHeight)*math.tan(maxSlope*Mathf.Deg2Rad)));
// Temporary arrays used for rasterization
var clipperOrig = new VoxelPolygonClipper();
var clipperX1 = new VoxelPolygonClipper();
var clipperX2 = new VoxelPolygonClipper();
var clipperZ1 = new VoxelPolygonClipper();
var clipperZ2 = new VoxelPolygonClipper();
// Find the largest lengths of vertex arrays and check for meshes which can be skipped
int maxVerts = 0;
for (int m = 0; m < bucket.Length; m++) {
maxVerts = math.max(inputMeshes[bucket[m]].vertices.Length, maxVerts);
}
// Create buffer, here vertices will be stored multiplied with the local-to-voxel-space matrix
var verts = new NativeArray<float3>(maxVerts, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
int width = voxelArea.width;
int depth = voxelArea.depth;
// These will be width-1 and depth-1 respectively for all but the last tile row and column of the graph
var cropX = Mathf.Min(width - 1, float.IsPositiveInfinity(graphSpaceLimits.x) ? int.MaxValue : Mathf.CeilToInt((graphSpaceLimits.x - graphSpaceBounds.min.x) / cellSize));
var cropZ = Mathf.Min(depth - 1, float.IsPositiveInfinity(graphSpaceLimits.y) ? int.MaxValue : Mathf.CeilToInt((graphSpaceLimits.y - graphSpaceBounds.min.z) / cellSize));
// This loop is the hottest place in the whole rasterization process
// it usually accounts for around 50% of the time
for (int m = 0; m < bucket.Length; m++) {
RasterizationMesh mesh = inputMeshes[bucket[m]];
var meshMatrix = mesh.matrix;
// Flip the orientation of all faces if the mesh is scaled in such a way
// that the face orientations would change
// This happens for example if a mesh has a negative scale along an odd number of axes
// e.g it happens for the scale (-1, 1, 1) but not for (-1, -1, 1) or (1,1,1)
var flipOrientation = VectorMath.ReversesFaceOrientations(meshMatrix);
var vs = mesh.vertices;
var tris = mesh.triangles;
// Transform vertices first to world space and then to voxel space
var localToVoxelMatrix = (float4x4)(world2voxelMatrix * mesh.matrix);
for (int i = 0; i < vs.Length; i++) verts[i] = math.transform(localToVoxelMatrix, vs[i]);
int mesharea = mesh.area;
if (mesh.areaIsTag) {
mesharea |= VoxelUtilityBurst.TagReg;
}
var meshBounds = new IntRect();
for (int i = 0; i < tris.Length; i += 3) {
float3 p1 = verts[tris[i]];
float3 p2 = verts[tris[i+1]];
float3 p3 = verts[tris[i+2]];
if (flipOrientation) {
var tmp = p1;
p1 = p3;
p3 = tmp;
}
int minX = (int)math.min(math.min(p1.x, p2.x), p3.x);
int minZ = (int)math.min(math.min(p1.z, p2.z), p3.z);
int maxX = (int)math.ceil(math.max(math.max(p1.x, p2.x), p3.x));
int maxZ = (int)math.ceil(math.max(math.max(p1.z, p2.z), p3.z));
// Check if the mesh is completely out of bounds
if (minX > cropX || minZ > cropZ || maxX < 0 || maxZ < 0) continue;
minX = math.clamp(minX, 0, cropX);
maxX = math.clamp(maxX, 0, cropX);
minZ = math.clamp(minZ, 0, cropZ);
maxZ = math.clamp(maxZ, cropZ, cropZ);
if (i == 0) meshBounds = new IntRect(minX, minZ, minX, minZ);
meshBounds.xmin = math.min(meshBounds.xmin, minX);
meshBounds.xmax = math.max(meshBounds.xmax, maxX);
meshBounds.ymin = math.min(meshBounds.ymin, minZ);
meshBounds.ymax = math.max(meshBounds.ymax, maxZ);
// Check max slope
float3 normal = math.cross(p2-p1, p3-p1);
float cosSlopeAngle = math.normalizesafe(normal).y;
if (mesh.doubleSided) cosSlopeAngle = math.abs(cosSlopeAngle);
int area = cosSlopeAngle < slopeLimit ? CompactVoxelField.UnwalkableArea : 1 + mesharea;
clipperOrig[0] = p1;
clipperOrig[1] = p2;
clipperOrig[2] = p3;
clipperOrig.n = 3;
for (int x = minX; x <= maxX; x++) {
clipperOrig.ClipPolygonAlongX(ref clipperX1, 1f, -x+0.5f);
if (clipperX1.n < 3) {
continue;
}
clipperX1.ClipPolygonAlongX(ref clipperX2, -1F, x+0.5F);
if (clipperX2.n < 3) {
continue;
}
float clampZ1, clampZ2;
unsafe {
clampZ1 = clampZ2 = clipperX2.z[0];
for (int q = 1; q < clipperX2.n; q++) {
float val = clipperX2.z[q];
clampZ1 = math.min(clampZ1, val);
clampZ2 = math.max(clampZ2, val);
}
}
int clampZ1I = math.clamp((int)math.round(clampZ1), 0, cropX);
int clampZ2I = math.clamp((int)math.round(clampZ2), 0, cropZ);
for (int z = clampZ1I; z <= clampZ2I; z++) {
clipperX2.ClipPolygonAlongZWithYZ(ref clipperZ1, 1F, -z+0.5F);
if (clipperZ1.n < 3) {
continue;
}
clipperZ1.ClipPolygonAlongZWithY(ref clipperZ2, -1F, z+0.5F);
if (clipperZ2.n < 3) {
continue;
}
if (mesh.flatten) {
voxelArea.AddFlattenedSpan(z*width+x, area);
} else {
float sMin, sMax;
unsafe {
var u = clipperZ2.y[0];
sMin = sMax = u;
for (int q = 1; q < clipperZ2.n; q++) {
float val = clipperZ2.y[q];
sMin = math.min(sMin, val);
sMax = math.max(sMax, val);
}
}
int maxi = (int)math.ceil(sMax);
// Make sure mini >= 0
int mini = (int)sMin;
// Make sure the span is at least 1 voxel high
maxi = math.max(mini+1, maxi);
voxelArea.AddLinkedSpan(z*width+x, mini, maxi, area, voxelWalkableClimb, m);
}
}
}
}
if (mesh.solid) {
for (int z = meshBounds.ymin; z <= meshBounds.ymax; z++) {
for (int x = meshBounds.xmin; x <= meshBounds.xmax; x++) {
voxelArea.ResolveSolid(z*voxelArea.width + x, m, voxelWalkableClimb);
}
}
}
}
}
}
[BurstCompile(CompileSynchronously = true)]
struct JobBuildCompactField : IJob {
public LinkedVoxelField input;
public CompactVoxelField output;
public void Execute () {
output.BuildFromLinkedField(input);
}
}
[BurstCompile(CompileSynchronously = true)]
struct JobBuildConnections : IJob {
public CompactVoxelField field;
public int voxelWalkableHeight;
public int voxelWalkableClimb;
public void Execute () {
int wd = field.width*field.depth;
// Build voxel connections
for (int z = 0, pz = 0; z < wd; z += field.width, pz++) {
for (int x = 0; x < field.width; x++) {
CompactVoxelCell c = field.cells[x+z];
for (int i = (int)c.index, ni = (int)(c.index+c.count); i < ni; i++) {
CompactVoxelSpan s = field.spans[i];
s.con = 0xFFFFFFFF;
for (int d = 0; d < 4; d++) {
int nx = x+VoxelUtilityBurst.DX[d];
int nz = z+VoxelUtilityBurst.DZ[d]*field.width;
if (nx < 0 || nz < 0 || nz >= wd || nx >= field.width) {
continue;
}
CompactVoxelCell nc = field.cells[nx+nz];
for (int k = nc.index, nk = (int)(nc.index+nc.count); k < nk; k++) {
CompactVoxelSpan ns = field.spans[k];
int bottom = System.Math.Max(s.y, ns.y);
int top = System.Math.Min((int)s.y+(int)s.h, (int)ns.y+(int)ns.h);
if ((top-bottom) >= voxelWalkableHeight && System.Math.Abs((int)ns.y - (int)s.y) <= voxelWalkableClimb) {
uint connIdx = (uint)k - (uint)nc.index;
if (connIdx > CompactVoxelField.MaxLayers) {
#if ENABLE_UNITY_COLLECTIONS_CHECKS
throw new System.Exception("Too many layers");
#else
break;
#endif
}
s.SetConnection(d, connIdx);
break;
}
}
}
field.spans[i] = s;
}
}
}
}
}
[BurstCompile(CompileSynchronously = true)]
struct JobErodeWalkableArea : IJob {
public CompactVoxelField field;
public int radius;
public void Execute () {
var distances = new NativeArray<ushort>(field.spans.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
VoxelUtilityBurst.CalculateDistanceField(field, distances);
for (int i = 0; i < distances.Length; i++) {
// Note multiplied with 2 because the distance field increments distance by 2 for each voxel (and 3 for diagonal)
if (distances[i] < radius*2) {
field.areaTypes[i] = CompactVoxelField.UnwalkableArea;
}
}
}
}
[BurstCompile(CompileSynchronously = true)]
struct JobBuildDistanceField : IJob {
public CompactVoxelField field;
public NativeList<ushort> output;
public void Execute () {
var distances = new NativeArray<ushort>(field.spans.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
VoxelUtilityBurst.CalculateDistanceField(field, distances);
output.ResizeUninitialized(field.spans.Length);
VoxelUtilityBurst.BoxBlur(field, distances, output.AsArray());
}
}
[BurstCompile(CompileSynchronously = true)]
struct JobFilterLowHeightSpans : IJob {
public LinkedVoxelField field;
public uint voxelWalkableHeight;
public void Execute () {
int wd = field.width*field.depth;
//Filter all ledges
var spans = field.linkedSpans;
for (int z = 0, pz = 0; z < wd; z += field.width, pz++) {
for (int x = 0; x < field.width; x++) {
for (int s = z+x; s != -1 && spans[s].bottom != LinkedVoxelField.InvalidSpanValue; s = spans[s].next) {
uint bottom = spans[s].top;
uint top = spans[s].next != -1 ? spans[spans[s].next].bottom : LinkedVoxelField.MaxHeight;
if (top - bottom < voxelWalkableHeight) {
var span = spans[s];
span.area = CompactVoxelField.UnwalkableArea;
spans[s] = span;
}
}
}
}
}
}
[BurstCompile(CompileSynchronously = true)]
struct JobFilterLedges : IJob {
public LinkedVoxelField field;
public uint voxelWalkableHeight;
public int voxelWalkableClimb;
public float cellSize;
public float cellHeight;
// Code almost completely ripped from Recast
public void Execute () {
// Use an UnsafeSpan to be able to use the ref-return values in order to directly assign fields on spans.
var spans = field.linkedSpans.AsUnsafeSpan();
int wd = field.width*field.depth;
int width = field.width;
// Filter all ledges
for (int z = 0, pz = 0; z < wd; z += width, pz++) {
for (int x = 0; x < width; x++) {
if (spans[x+z].bottom == LinkedVoxelField.InvalidSpanValue) continue;
for (int s = x+z; s != -1; s = spans[s].next) {
// Skip non-walkable spans
if (spans[s].area == CompactVoxelField.UnwalkableArea) {
continue;
}
// Points on the edge of the voxel field will always have at least 1 out-of-bounds neighbour
if (x == 0 || z == 0 || z == (wd-width) || x == (width-1)) {
spans[s].area = CompactVoxelField.UnwalkableArea;
continue;
}
int bottom = (int)spans[s].top;
int top = spans[s].next != -1 ? (int)spans[spans[s].next].bottom : (int)LinkedVoxelField.MaxHeight;
// Find neighbours' minimum height.
int minNeighborHeight = (int)LinkedVoxelField.MaxHeight;
// Min and max height of accessible neighbours.
int accessibleNeighborMinHeight = (int)spans[s].top;
int accessibleNeighborMaxHeight = accessibleNeighborMinHeight;
for (int d = 0; d < 4; d++) {
int nx = x + VoxelUtilityBurst.DX[d];
int nz = z + VoxelUtilityBurst.DZ[d]*width;
int nsx = nx+nz;
int nbottom = -voxelWalkableClimb;
int ntop = spans[nsx].bottom != LinkedVoxelField.InvalidSpanValue ? (int)spans[nsx].bottom : (int)LinkedVoxelField.MaxHeight;
// Skip neighbour if the gap between the spans is too small.
if (math.min(top, ntop) - math.max(bottom, nbottom) > voxelWalkableHeight) {
minNeighborHeight = math.min(minNeighborHeight, nbottom - bottom);
}
// Loop through the rest of the spans
if (spans[nsx].bottom != LinkedVoxelField.InvalidSpanValue) {
for (int ns = nsx; ns != -1; ns = spans[ns].next) {
ref var nSpan = ref spans[ns];
nbottom = (int)nSpan.top;
// Break the loop if it is no longer possible for the spans to overlap.
// This is purely a performance optimization
if (nbottom > top - voxelWalkableHeight) break;
ntop = nSpan.next != -1 ? (int)spans[nSpan.next].bottom : (int)LinkedVoxelField.MaxHeight;
// Check the overlap of the ranges (bottom,top) and (nbottom,ntop)
// This is the minimum height when moving from the top surface of span #s to the top surface of span #ns
if (math.min(top, ntop) - math.max(bottom, nbottom) > voxelWalkableHeight) {
minNeighborHeight = math.min(minNeighborHeight, nbottom - bottom);
// Find min/max accessible neighbour height.
if (math.abs(nbottom - bottom) <= voxelWalkableClimb) {
if (nbottom < accessibleNeighborMinHeight) { accessibleNeighborMinHeight = nbottom; }
if (nbottom > accessibleNeighborMaxHeight) { accessibleNeighborMaxHeight = nbottom; }
}
}
}
}
}
// The current span is close to a ledge if the drop to any
// neighbour span is less than the walkableClimb.
// Additionally, if the difference between all neighbours is too large,
// we are at steep slope: mark the span as ledge.
if (minNeighborHeight < -voxelWalkableClimb || (accessibleNeighborMaxHeight - accessibleNeighborMinHeight) > voxelWalkableClimb) {
spans[s].area = CompactVoxelField.UnwalkableArea;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af78ae0fb20c2907695f4acc47d811a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,803 @@
using UnityEngine;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Burst;
using Pathfinding.Util;
namespace Pathfinding.Graphs.Navmesh.Voxelization.Burst {
[BurstCompile(CompileSynchronously = true)]
public struct JobBuildRegions : IJob {
public CompactVoxelField field;
public NativeList<ushort> distanceField;
public int borderSize;
public int minRegionSize;
public NativeQueue<Int3> srcQue;
public NativeQueue<Int3> dstQue;
public RecastGraph.RelevantGraphSurfaceMode relevantGraphSurfaceMode;
public NativeArray<RelevantGraphSurfaceInfo> relevantGraphSurfaces;
public float cellSize, cellHeight;
public Matrix4x4 graphTransform;
public Bounds graphSpaceBounds;
void MarkRectWithRegion (int minx, int maxx, int minz, int maxz, ushort region, NativeArray<ushort> srcReg) {
int md = maxz * field.width;
for (int z = minz*field.width; z < md; z += field.width) {
for (int x = minx; x < maxx; x++) {
CompactVoxelCell c = field.cells[z+x];
for (int i = c.index, ni = c.index+c.count; i < ni; i++) {
if (field.areaTypes[i] != CompactVoxelField.UnwalkableArea) {
srcReg[i] = region;
}
}
}
}
}
public static bool FloodRegion (int x, int z, int i, uint level, ushort r,
CompactVoxelField field,
NativeArray<ushort> distanceField,
NativeArray<ushort> srcReg,
NativeArray<ushort> srcDist,
NativeArray<Int3> stack,
NativeArray<int> flags,
NativeArray<bool> closed) {
int area = field.areaTypes[i];
// Flood f mark region.
int stackSize = 1;
stack[0] = new Int3 {
x = x,
y = i,
z = z,
};
srcReg[i] = r;
srcDist[i] = 0;
int lev = (int)(level >= 2 ? level-2 : 0);
int count = 0;
// Store these in local variables (for performance, avoids an extra indirection)
var compactCells = field.cells;
var compactSpans = field.spans;
var areaTypes = field.areaTypes;
while (stackSize > 0) {
stackSize--;
var c = stack[stackSize];
//Similar to the Pop operation of an array, but Pop is not implemented in List<>
int ci = c.y;
int cx = c.x;
int cz = c.z;
CompactVoxelSpan cs = compactSpans[ci];
// Check if any of the neighbours already have a valid region set.
ushort ar = 0;
// Loop through four neighbours
// then check one neighbour of the neighbour
// to get the diagonal neighbour
for (int dir = 0; dir < 4; dir++) {
// 8 connected
if (cs.GetConnection(dir) != CompactVoxelField.NotConnected) {
int ax = cx + VoxelUtilityBurst.DX[dir];
int az = cz + VoxelUtilityBurst.DZ[dir]*field.width;
int ai = (int)compactCells[ax+az].index + cs.GetConnection(dir);
if (areaTypes[ai] != area)
continue;
ushort nr = srcReg[ai];
if ((nr & VoxelUtilityBurst.BorderReg) == VoxelUtilityBurst.BorderReg) // Do not take borders into account.
continue;
if (nr != 0 && nr != r) {
ar = nr;
// Found a valid region, skip checking the rest
break;
}
// Rotate dir 90 degrees
int dir2 = (dir+1) & 0x3;
var neighbour2 = compactSpans[ai].GetConnection(dir2);
// Check the diagonal connection
if (neighbour2 != CompactVoxelField.NotConnected) {
int ax2 = ax + VoxelUtilityBurst.DX[dir2];
int az2 = az + VoxelUtilityBurst.DZ[dir2]*field.width;
int ai2 = compactCells[ax2+az2].index + neighbour2;
if (areaTypes[ai2] != area)
continue;
ushort nr2 = srcReg[ai2];
if ((nr2 & VoxelUtilityBurst.BorderReg) == VoxelUtilityBurst.BorderReg) // Do not take borders into account.
continue;
if (nr2 != 0 && nr2 != r) {
ar = nr2;
// Found a valid region, skip checking the rest
break;
}
}
}
}
if (ar != 0) {
srcReg[ci] = 0;
srcDist[ci] = 0xFFFF;
continue;
}
count++;
closed[ci] = true;
// Expand neighbours.
for (int dir = 0; dir < 4; ++dir) {
if (cs.GetConnection(dir) == CompactVoxelField.NotConnected) continue;
int ax = cx + VoxelUtilityBurst.DX[dir];
int az = cz + VoxelUtilityBurst.DZ[dir]*field.width;
int ai = compactCells[ax+az].index + cs.GetConnection(dir);
if (areaTypes[ai] != area) continue;
if (srcReg[ai] != 0) continue;
if (distanceField[ai] >= lev && flags[ai] == 0) {
srcReg[ai] = r;
srcDist[ai] = 0;
stack[stackSize] = new Int3 {
x = ax,
y = ai,
z = az,
};
stackSize++;
} else {
flags[ai] = r;
srcDist[ai] = 2;
}
}
}
return count > 0;
}
public void Execute () {
srcQue.Clear();
dstQue.Clear();
int w = field.width;
int d = field.depth;
int wd = w*d;
int spanCount = field.spans.Length;
int expandIterations = 8;
var srcReg = new NativeArray<ushort>(spanCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var srcDist = new NativeArray<ushort>(spanCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var closed = new NativeArray<bool>(spanCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var spanFlags = new NativeArray<int>(spanCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var stack = new NativeArray<Int3>(spanCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
// The array pool arrays may contain arbitrary data. We need to zero it out.
for (int i = 0; i < spanCount; i++) {
srcReg[i] = 0;
srcDist[i] = 0xFFFF;
closed[i] = false;
spanFlags[i] = 0;
}
var spanDistances = distanceField;
var areaTypes = field.areaTypes;
var compactCells = field.cells;
const ushort BorderReg = VoxelUtilityBurst.BorderReg;
ushort regionId = 2;
MarkRectWithRegion(0, borderSize, 0, d, (ushort)(regionId | BorderReg), srcReg); regionId++;
MarkRectWithRegion(w-borderSize, w, 0, d, (ushort)(regionId | BorderReg), srcReg); regionId++;
MarkRectWithRegion(0, w, 0, borderSize, (ushort)(regionId | BorderReg), srcReg); regionId++;
MarkRectWithRegion(0, w, d-borderSize, d, (ushort)(regionId | BorderReg), srcReg); regionId++;
// TODO: Can be optimized
int maxDistance = 0;
for (int i = 0; i < distanceField.Length; i++) {
maxDistance = math.max(distanceField[i], maxDistance);
}
// A distance is 2 to an adjacent span and 1 for a diagonally adjacent one.
NativeArray<int> sortedSpanCounts = new NativeArray<int>((maxDistance)/2 + 1, Allocator.Temp);
for (int i = 0; i < field.spans.Length; i++) {
// Do not take borders or unwalkable spans into account.
if ((srcReg[i] & BorderReg) == BorderReg || areaTypes[i] == CompactVoxelField.UnwalkableArea)
continue;
sortedSpanCounts[distanceField[i]/2]++;
}
var distanceIndexOffsets = new NativeArray<int>(sortedSpanCounts.Length, Allocator.Temp);
for (int i = 1; i < distanceIndexOffsets.Length; i++) {
distanceIndexOffsets[i] = distanceIndexOffsets[i-1] + sortedSpanCounts[i-1];
}
var totalRelevantSpans = distanceIndexOffsets[distanceIndexOffsets.Length - 1] + sortedSpanCounts[sortedSpanCounts.Length - 1];
var bucketSortedSpans = new NativeArray<Int3>(totalRelevantSpans, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
// Bucket sort the spans based on distance
for (int z = 0, pz = 0; z < wd; z += w, pz++) {
for (int x = 0; x < field.width; x++) {
CompactVoxelCell c = compactCells[z+x];
for (int i = c.index, ni = c.index+c.count; i < ni; i++) {
// Do not take borders or unwalkable spans into account.
if ((srcReg[i] & BorderReg) == BorderReg || areaTypes[i] == CompactVoxelField.UnwalkableArea)
continue;
int distIndex = distanceField[i] / 2;
bucketSortedSpans[distanceIndexOffsets[distIndex]++] = new Int3(x, i, z);
}
}
}
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if (distanceIndexOffsets[distanceIndexOffsets.Length - 1] != totalRelevantSpans) throw new System.Exception("Unexpected span count");
#endif
// Go through spans in reverse order (i.e largest distances first)
for (int distIndex = sortedSpanCounts.Length - 1; distIndex >= 0; distIndex--) {
var level = (uint)distIndex * 2;
var spansAtLevel = sortedSpanCounts[distIndex];
for (int i = 0; i < spansAtLevel; i++) {
// Go through the spans stored in bucketSortedSpans for this distance index.
// Note that distanceIndexOffsets[distIndex] will point to the element after the end of the group of spans.
// There is no particular reason for this, the code just turned out to be a bit simpler to implemen that way.
var spanInfo = bucketSortedSpans[distanceIndexOffsets[distIndex] - i - 1];
int spanIndex = spanInfo.y;
// This span is adjacent to a region, so we should start the BFS search from it
if (spanFlags[spanIndex] != 0 && srcReg[spanIndex] == 0) {
srcReg[spanIndex] = (ushort)spanFlags[spanIndex];
srcQue.Enqueue(spanInfo);
closed[spanIndex] = true;
}
}
// Expand a few iterations out from every known node
for (int expansionIteration = 0; expansionIteration < expandIterations && srcQue.Count > 0; expansionIteration++) {
while (srcQue.Count > 0) {
Int3 spanInfo = srcQue.Dequeue();
var area = areaTypes[spanInfo.y];
var span = field.spans[spanInfo.y];
var region = srcReg[spanInfo.y];
closed[spanInfo.y] = true;
ushort nextDist = (ushort)(srcDist[spanInfo.y] + 2);
// Go through the neighbours of the span
for (int dir = 0; dir < 4; dir++) {
var neighbour = span.GetConnection(dir);
if (neighbour == CompactVoxelField.NotConnected) continue;
int nx = spanInfo.x + VoxelUtilityBurst.DX[dir];
int nz = spanInfo.z + VoxelUtilityBurst.DZ[dir]*field.width;
int ni = compactCells[nx+nz].index + neighbour;
if ((srcReg[ni] & BorderReg) == BorderReg) // Do not take borders into account.
continue;
// Do not combine different area types
if (area == areaTypes[ni]) {
if (nextDist < srcDist[ni]) {
if (spanDistances[ni] < level) {
srcDist[ni] = nextDist;
spanFlags[ni] = region;
} else if (!closed[ni]) {
srcDist[ni] = nextDist;
if (srcReg[ni] == 0) dstQue.Enqueue(new Int3(nx, ni, nz));
srcReg[ni] = region;
}
}
}
}
}
Memory.Swap(ref srcQue, ref dstQue);
}
// Find the first span that has not been seen yet and start a new region that expands from there
var distanceFieldArr = distanceField.AsArray();
for (int i = 0; i < spansAtLevel; i++) {
var info = bucketSortedSpans[distanceIndexOffsets[distIndex] - i - 1];
if (srcReg[info.y] == 0) {
if (!FloodRegion(info.x, info.z, info.y, level, regionId, field, distanceFieldArr, srcReg, srcDist, stack, spanFlags, closed)) {
// The starting voxel was already adjacent to an existing region so we skip flooding it.
// It will be visited in the next area expansion.
} else {
regionId++;
}
}
}
}
var maxRegions = regionId;
// Transform from voxel space to graph space.
// then scale from voxel space (one unit equals one voxel)
// Finally add min
Matrix4x4 voxelMatrix = Matrix4x4.TRS(graphSpaceBounds.min, Quaternion.identity, Vector3.one) * Matrix4x4.Scale(new Vector3(cellSize, cellHeight, cellSize));
// Transform from voxel space to world space
// add half a voxel to fix rounding
var voxel2worldMatrix = graphTransform * voxelMatrix * Matrix4x4.Translate(new Vector3(0.5f, 0, 0.5f));
// Filter out small regions.
FilterSmallRegions(field, srcReg, minRegionSize, maxRegions, this.relevantGraphSurfaces, this.relevantGraphSurfaceMode, voxel2worldMatrix);
// Write the result out.
for (int i = 0; i < spanCount; i++) {
var span = field.spans[i];
span.reg = srcReg[i];
field.spans[i] = span;
}
// TODO:
// field.maxRegions = maxRegions;
// #if ASTAR_DEBUGREPLAY
// DebugReplay.BeginGroup("Regions");
// for (int z = 0, pz = 0; z < wd; z += field.width, pz++) {
// for (int x = 0; x < field.width; x++) {
// CompactVoxelCell c = field.cells[x+z];
// for (int i = (int)c.index; i < c.index+c.count; i++) {
// CompactVoxelSpan s = field.spans[i];
// DebugReplay.DrawCube(CompactSpanToVector(x, pz, i), UnityEngine.Vector3.one*cellSize, AstarMath.IntToColor(s.reg, 1.0f));
// }
// }
// }
// DebugReplay.EndGroup();
// int maxDist = 0;
// for (int i = 0; i < srcDist.Length; i++) if (srcDist[i] != 0xFFFF) maxDist = Mathf.Max(maxDist, srcDist[i]);
// DebugReplay.BeginGroup("Distances");
// for (int z = 0, pz = 0; z < wd; z += field.width, pz++) {
// for (int x = 0; x < field.width; x++) {
// CompactVoxelCell c = field.cells[x+z];
// for (int i = (int)c.index; i < c.index+c.count; i++) {
// CompactVoxelSpan s = field.spans[i];
// float f = (float)srcDist[i]/maxDist;
// DebugReplay.DrawCube(CompactSpanToVector(x, z/field.width, i), Vector3.one*cellSize, new Color(f, f, f));
// }
// }
// }
// DebugReplay.EndGroup();
// #endif
}
/// <summary>
/// Find method in the UnionFind data structure.
/// See: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
/// </summary>
static int union_find_find (NativeArray<int> arr, int x) {
if (arr[x] < 0) return x;
return arr[x] = union_find_find(arr, arr[x]);
}
/// <summary>
/// Join method in the UnionFind data structure.
/// See: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
/// </summary>
static void union_find_union (NativeArray<int> arr, int a, int b) {
a = union_find_find(arr, a);
b = union_find_find(arr, b);
if (a == b) return;
if (arr[a] > arr[b]) {
int tmp = a;
a = b;
b = tmp;
}
arr[a] += arr[b];
arr[b] = a;
}
public struct RelevantGraphSurfaceInfo {
public float3 position;
public float range;
}
/// <summary>Filters out or merges small regions.</summary>
public static void FilterSmallRegions (CompactVoxelField field, NativeArray<ushort> reg, int minRegionSize, int maxRegions, NativeArray<RelevantGraphSurfaceInfo> relevantGraphSurfaces, RecastGraph.RelevantGraphSurfaceMode relevantGraphSurfaceMode, float4x4 voxel2worldMatrix) {
// RelevantGraphSurface c = RelevantGraphSurface.Root;
// Need to use ReferenceEquals because it might be called from another thread
bool anySurfaces = relevantGraphSurfaces.Length != 0 && (relevantGraphSurfaceMode != RecastGraph.RelevantGraphSurfaceMode.DoNotRequire);
// Nothing to do here
if (!anySurfaces && minRegionSize <= 0) {
return;
}
var counter = new NativeArray<int>(maxRegions, Allocator.Temp);
var bits = new NativeArray<ushort>(maxRegions, Allocator.Temp, NativeArrayOptions.ClearMemory);
for (int i = 0; i < counter.Length; i++) counter[i] = -1;
int nReg = counter.Length;
int wd = field.width*field.depth;
const int RelevantSurfaceSet = 1 << 1;
const int BorderBit = 1 << 0;
// Mark RelevantGraphSurfaces
const ushort BorderReg = VoxelUtilityBurst.BorderReg;
// If they can also be adjacent to tile borders, this will also include the BorderBit
int RelevantSurfaceCheck = RelevantSurfaceSet | ((relevantGraphSurfaceMode == RecastGraph.RelevantGraphSurfaceMode.OnlyForCompletelyInsideTile) ? BorderBit : 0x0);
// int RelevantSurfaceCheck = 0;
if (anySurfaces) {
var world2voxelMatrix = math.inverse(voxel2worldMatrix);
for (int j = 0; j < relevantGraphSurfaces.Length; j++) {
var relevantGraphSurface = relevantGraphSurfaces[j];
var positionInVoxelSpace = math.transform(world2voxelMatrix, relevantGraphSurface.position);
int3 cellIndex = (int3)math.round(positionInVoxelSpace);
// Check for out of bounds
if (cellIndex.x >= 0 && cellIndex.z >= 0 && cellIndex.x < field.width && cellIndex.z < field.depth) {
var yScaleFactor = math.length(voxel2worldMatrix.c1.xyz);
int rad = (int)(relevantGraphSurface.range / yScaleFactor);
CompactVoxelCell cell = field.cells[cellIndex.x+cellIndex.z*field.width];
for (int i = cell.index; i < cell.index+cell.count; i++) {
CompactVoxelSpan s = field.spans[i];
if (System.Math.Abs(s.y - cellIndex.y) <= rad && reg[i] != 0) {
bits[union_find_find(counter, reg[i] & ~BorderReg)] |= RelevantSurfaceSet;
}
}
}
}
}
for (int z = 0; z < wd; z += field.width) {
for (int x = 0; x < field.width; x++) {
CompactVoxelCell cell = field.cells[x+z];
for (int i = cell.index; i < cell.index+cell.count; i++) {
CompactVoxelSpan s = field.spans[i];
int r = reg[i];
// Check if this is an unwalkable span
if ((r & ~BorderReg) == 0) continue;
if (r >= nReg) { //Probably border
bits[union_find_find(counter, r & ~BorderReg)] |= BorderBit;
continue;
}
int root = union_find_find(counter, r);
// Count this span
counter[root]--;
// Iterate through all neighbours of the span.
for (int dir = 0; dir < 4; dir++) {
if (s.GetConnection(dir) == CompactVoxelField.NotConnected) { continue; }
int nx = x + VoxelUtilityBurst.DX[dir];
int nz = z + VoxelUtilityBurst.DZ[dir] * field.width;
int ni = field.cells[nx+nz].index + s.GetConnection(dir);
int r2 = reg[ni];
// Check if the other span belongs to a different region and is walkable
if (r != r2 && (r2 & ~BorderReg) != 0) {
if ((r2 & BorderReg) != 0) {
// If it's a border region we just mark the current region as being adjacent to a border
bits[root] |= BorderBit;
} else {
// Join the adjacent region with this region.
union_find_union(counter, root, r2);
}
//counter[r] = minRegionSize;
}
}
//counter[r]++;
}
}
}
// Propagate bits to the region group representative using the union find structure
for (int i = 0; i < counter.Length; i++) bits[union_find_find(counter, i)] |= bits[i];
for (int i = 0; i < counter.Length; i++) {
int ctr = union_find_find(counter, i);
// Check if the region is adjacent to border.
// Mark it as being just large enough to always be included in the graph.
if ((bits[ctr] & BorderBit) != 0) counter[ctr] = -minRegionSize-2;
// Not in any relevant surface
// or it is adjacent to a border (see RelevantSurfaceCheck)
if (anySurfaces && (bits[ctr] & RelevantSurfaceCheck) == 0) counter[ctr] = -1;
}
for (int i = 0; i < reg.Length; i++) {
int r = reg[i];
// Ignore border regions
if (r >= nReg) {
continue;
}
// If the region group is too small then make the span unwalkable
if (counter[union_find_find(counter, r)] >= -minRegionSize-1) {
reg[i] = 0;
}
}
}
}
static class VoxelUtilityBurst {
/// <summary>All bits in the region which will be interpreted as a tag.</summary>
public const int TagRegMask = TagReg - 1;
/// <summary>
/// If a cell region has this bit set then
/// The remaining region bits (see <see cref="TagRegMask)"/> will be used for the node's tag.
/// </summary>
public const int TagReg = 1 << 14;
/// <summary>
/// If heightfield region ID has the following bit set, the region is on border area
/// and excluded from many calculations.
/// </summary>
public const ushort BorderReg = 1 << 15;
/// <summary>
/// If contour region ID has the following bit set, the vertex will be later
/// removed in order to match the segments and vertices at tile boundaries.
/// </summary>
public const int RC_BORDER_VERTEX = 1 << 16;
public const int RC_AREA_BORDER = 1 << 17;
public const int VERTEX_BUCKET_COUNT = 1<<12;
/// <summary>Tessellate wall edges</summary>
public const int RC_CONTOUR_TESS_WALL_EDGES = 1 << 0;
/// <summary>Tessellate edges between areas</summary>
public const int RC_CONTOUR_TESS_AREA_EDGES = 1 << 1;
/// <summary>Tessellate edges at the border of the tile</summary>
public const int RC_CONTOUR_TESS_TILE_EDGES = 1 << 2;
/// <summary>Mask used with contours to extract region id.</summary>
public const int ContourRegMask = 0xffff;
public static readonly int[] DX = new int[] { -1, 0, 1, 0 };
public static readonly int[] DZ = new int[] { 0, 1, 0, -1 };
public static void CalculateDistanceField (CompactVoxelField field, NativeArray<ushort> output) {
int wd = field.width*field.depth;
// Mark boundary cells
for (int z = 0; z < wd; z += field.width) {
for (int x = 0; x < field.width; x++) {
CompactVoxelCell c = field.cells[x+z];
for (int i = c.index, ci = c.index+c.count; i < ci; i++) {
CompactVoxelSpan s = field.spans[i];
int numConnections = 0;
for (int d = 0; d < 4; d++) {
if (s.GetConnection(d) != CompactVoxelField.NotConnected) {
//This function (CalculateDistanceField) is used for both ErodeWalkableArea and by itself.
//The C++ recast source uses different code for those two cases, but I have found it works with one function
//the field.areaTypes[ni] will actually only be one of two cases when used from ErodeWalkableArea
//so it will have the same effect as
// if (area != UnwalkableArea) {
//This line is the one where the differ most
numConnections++;
} else {
break;
}
}
// TODO: Check initialization
output[i] = numConnections == 4 ? ushort.MaxValue : (ushort)0;
}
}
}
// Grassfire transform
// Pass 1
for (int z = 0; z < wd; z += field.width) {
for (int x = 0; x < field.width; x++) {
int cellIndex = x + z;
CompactVoxelCell c = field.cells[cellIndex];
for (int i = c.index, ci = c.index+c.count; i < ci; i++) {
CompactVoxelSpan s = field.spans[i];
var dist = (int)output[i];
if (s.GetConnection(0) != CompactVoxelField.NotConnected) {
// (-1,0)
int neighbourCell = field.GetNeighbourIndex(cellIndex, 0);
int ni = field.cells[neighbourCell].index+s.GetConnection(0);
dist = math.min(dist, (int)output[ni]+2);
CompactVoxelSpan ns = field.spans[ni];
if (ns.GetConnection(3) != CompactVoxelField.NotConnected) {
// (-1,0) + (0,-1) = (-1,-1)
int neighbourCell2 = field.GetNeighbourIndex(neighbourCell, 3);
int nni = (int)(field.cells[neighbourCell2].index+ns.GetConnection(3));
dist = math.min(dist, (int)output[nni]+3);
}
}
if (s.GetConnection(3) != CompactVoxelField.NotConnected) {
// (0,-1)
int neighbourCell = field.GetNeighbourIndex(cellIndex, 3);
int ni = (int)(field.cells[neighbourCell].index+s.GetConnection(3));
dist = math.min(dist, (int)output[ni]+2);
CompactVoxelSpan ns = field.spans[ni];
if (ns.GetConnection(2) != CompactVoxelField.NotConnected) {
// (0,-1) + (1,0) = (1,-1)
int neighbourCell2 = field.GetNeighbourIndex(neighbourCell, 2);
int nni = (int)(field.cells[neighbourCell2].index+ns.GetConnection(2));
dist = math.min(dist, (int)output[nni]+3);
}
}
output[i] = (ushort)dist;
}
}
}
// Pass 2
for (int z = wd-field.width; z >= 0; z -= field.width) {
for (int x = field.width-1; x >= 0; x--) {
int cellIndex = x + z;
CompactVoxelCell c = field.cells[cellIndex];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
CompactVoxelSpan s = field.spans[i];
var dist = (int)output[i];
if (s.GetConnection(2) != CompactVoxelField.NotConnected) {
// (-1,0)
int neighbourCell = field.GetNeighbourIndex(cellIndex, 2);
int ni = (int)(field.cells[neighbourCell].index+s.GetConnection(2));
dist = math.min(dist, (int)output[ni]+2);
CompactVoxelSpan ns = field.spans[ni];
if (ns.GetConnection(1) != CompactVoxelField.NotConnected) {
// (-1,0) + (0,-1) = (-1,-1)
int neighbourCell2 = field.GetNeighbourIndex(neighbourCell, 1);
int nni = (int)(field.cells[neighbourCell2].index+ns.GetConnection(1));
dist = math.min(dist, (int)output[nni]+3);
}
}
if (s.GetConnection(1) != CompactVoxelField.NotConnected) {
// (0,-1)
int neighbourCell = field.GetNeighbourIndex(cellIndex, 1);
int ni = (int)(field.cells[neighbourCell].index+s.GetConnection(1));
dist = math.min(dist, (int)output[ni]+2);
CompactVoxelSpan ns = field.spans[ni];
if (ns.GetConnection(0) != CompactVoxelField.NotConnected) {
// (0,-1) + (1,0) = (1,-1)
int neighbourCell2 = field.GetNeighbourIndex(neighbourCell, 0);
int nni = (int)(field.cells[neighbourCell2].index+ns.GetConnection(0));
dist = math.min(dist, (int)output[nni]+3);
}
}
output[i] = (ushort)dist;
}
}
}
// #if ASTAR_DEBUGREPLAY && FALSE
// DebugReplay.BeginGroup("Distance Field");
// for (int z = wd-field.width; z >= 0; z -= field.width) {
// for (int x = field.width-1; x >= 0; x--) {
// CompactVoxelCell c = field.cells[x+z];
// for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
// DebugReplay.DrawCube(CompactSpanToVector(x, z/field.width, i), Vector3.one*cellSize, new Color((float)output[i]/maxDist, (float)output[i]/maxDist, (float)output[i]/maxDist));
// }
// }
// }
// DebugReplay.EndGroup();
// #endif
}
public static void BoxBlur (CompactVoxelField field, NativeArray<ushort> src, NativeArray<ushort> dst) {
ushort thr = 20;
int wd = field.width*field.depth;
for (int z = wd-field.width; z >= 0; z -= field.width) {
for (int x = field.width-1; x >= 0; x--) {
int cellIndex = x + z;
CompactVoxelCell c = field.cells[cellIndex];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
CompactVoxelSpan s = field.spans[i];
ushort cd = src[i];
if (cd < thr) {
dst[i] = cd;
continue;
}
int total = (int)cd;
for (int d = 0; d < 4; d++) {
if (s.GetConnection(d) != CompactVoxelField.NotConnected) {
var neighbourIndex = field.GetNeighbourIndex(cellIndex, d);
int ni = (int)(field.cells[neighbourIndex].index+s.GetConnection(d));
total += (int)src[ni];
CompactVoxelSpan ns = field.spans[ni];
int d2 = (d+1) & 0x3;
if (ns.GetConnection(d2) != CompactVoxelField.NotConnected) {
var neighbourIndex2 = field.GetNeighbourIndex(neighbourIndex, d2);
int nni = (int)(field.cells[neighbourIndex2].index+ns.GetConnection(d2));
total += (int)src[nni];
} else {
total += cd;
}
} else {
total += cd*2;
}
}
dst[i] = (ushort)((total+5)/9F);
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5b3bda46dccdc886959894545826304
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: