update
This commit is contained in:
381
Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs
Normal file
381
Packages/com.arongranberg.astar/Graphs/Navmesh/AABBTree.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 183e10f9cadca424792b5f940ce3fe3d
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
578
Packages/com.arongranberg.astar/Graphs/Navmesh/BBTree.cs
Normal file
578
Packages/com.arongranberg.astar/Graphs/Navmesh/BBTree.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a20480c673fd40a5bd2a4cc2206dbc4
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 65d5806c4978b7e46b69297ca838f91c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.arongranberg.astar/Graphs/Navmesh/Jobs.meta
Normal file
8
Packages/com.arongranberg.astar/Graphs/Navmesh/Jobs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d87dc471eec3ae4dac67ee232391350
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd51eca97d285874d997d22edd420a27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a22b53fa064d9344988e2a86b73851b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20aeb827260a74a4492e7687fdebb14f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30417132dbc15504abbdf1b70224c006
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dd00a18824d04764783722c547fb60f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 229fdb01207c1ab4796deea78744e136
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b86cf43938afd654a8f1b711e55977d7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff97d8db3ca9a074dbfbd83fa5ad16be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eea3ec9fc5dd8604c9902e09277d86d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
129
Packages/com.arongranberg.astar/Graphs/Navmesh/NavmeshTile.cs
Normal file
129
Packages/com.arongranberg.astar/Graphs/Navmesh/NavmeshTile.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7408cbadf2e744d22853a92b15abede1
|
||||
timeCreated: 1474405146
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6b7a26d35ca0154fa87ac69a555cce1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1192
Packages/com.arongranberg.astar/Graphs/Navmesh/RecastMeshGatherer.cs
Normal file
1192
Packages/com.arongranberg.astar/Graphs/Navmesh/RecastMeshGatherer.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b37acf4e486d51b8394c1d8e2b0c59c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
367
Packages/com.arongranberg.astar/Graphs/Navmesh/TileBuilder.cs
Normal file
367
Packages/com.arongranberg.astar/Graphs/Navmesh/TileBuilder.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bfefed1cddc88f449cc850ad00f2f77
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1173
Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs
Normal file
1173
Packages/com.arongranberg.astar/Graphs/Navmesh/TileHandler.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 664ab28b7671144dfa4515ea79a4c49e
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
112
Packages/com.arongranberg.astar/Graphs/Navmesh/TileLayout.cs
Normal file
112
Packages/com.arongranberg.astar/Graphs/Navmesh/TileLayout.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf2ae7ff6aabbdc4fa76468eedbf53f6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Packages/com.arongranberg.astar/Graphs/Navmesh/TileMesh.cs
Normal file
48
Packages/com.arongranberg.astar/Graphs/Navmesh/TileMesh.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16f2efac26c436946b764d2263a0a089
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
193
Packages/com.arongranberg.astar/Graphs/Navmesh/TileMeshes.cs
Normal file
193
Packages/com.arongranberg.astar/Graphs/Navmesh/TileMeshes.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e1e88f7c3e2d2c45ab0ba43bbce2cd4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce1c1f6432f234a46b5e914d99379d70
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddc46f5b05337b6ba8eae5dd4906634d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6e41a3dcfac38cd8910584fc5de0d39
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aba1f429a9dee0ef98d35221ff450cda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73110b746664b5ec197eda5f732356a5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10347e1eaceee428fa14386ccbaffde5
|
||||
timeCreated: 1454161567
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: af78ae0fb20c2907695f4acc47d811a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5b3bda46dccdc886959894545826304
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user