/*******************************************************************************
* Copyright 2015 See AUTHORS file.
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.mygdx.game.pathfinding;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ai.pfa.Connection;
import com.badlogic.gdx.ai.pfa.indexed.IndexedGraph;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.model.MeshPart;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ArrayMap;
import com.badlogic.gdx.utils.Bits;
import java.nio.FloatBuffer;
/**
* Creates a bidirectional graph over the triangles in the model, which can be used for A* pathfinding.
* All meshes in the model must be indexed (Mesh.getNumIndices()} > 0). The mesh can be divided into
* multiple MeshParts (useful for ignoring certain parts of the navmesh or perhaps triggering an event).
* All MeshParts should share some edge(s) with another in order for the mesh to be fully traversable.
* <p/>
* All meshes should be made up of one or more triangles, and should not have any isolated edges or vertices.
* <p/>
* Each vertex which has a unique position is stored in a Vector3. Triangle objects map which of these vertices
* form a triangle according to the winding order in the mesh indices buffer. The winding order is assumed to
* be the same for each triangle and is read from left to right in the indices buffer.
* <p/>
* Each triangle A which shares an edge with another triangle B is associated with an Edge/Connection object. In
* this object, triangle A is stored as the fromNode, B as toNode. The object also stores the vertices which
* makes up this edge, in the same winding order as triangle A. Additionally, since B is also connected to A,
* a mirrored edge is also stored along with B, where the edge has the same winding order as B.
* <p/>
* The reason the winding order is important is because each edge in the triangle must have the correct vertices
* defined as left/right in order for path smoothing to work correctly. Left/right is defined from the perspective
* of the centroid of the triangle when "looking" at the edge.
*
* @author jsjolund
*/
public class NavMeshGraph implements IndexedGraph<Triangle> {
/**
* Class for storing the edge connection data between two adjacent triangles.
*/
private static class IndexConnection {
// The vertex indices which makes up the edge shared between two triangles.
short edgeVertexIndex1;
short edgeVertexIndex2;
// The indices of the two triangles sharing this edge.
short fromTriIndex;
short toTriIndex;
public IndexConnection(short sharedEdgeVertex1Index, short edgeVertexIndex2,
short fromTriIndex, short toTriIndex) {
this.edgeVertexIndex1 = sharedEdgeVertex1Index;
this.edgeVertexIndex2 = edgeVertexIndex2;
this.fromTriIndex = fromTriIndex;
this.toTriIndex = toTriIndex;
}
}
private static final String TAG = "NavMeshGraph";
private final ArrayMap<Triangle, Array<Edge>> sharedEdges;
private final ArrayMap<Triangle, Array<Edge>> isolatedEdgesMap;
private int[] meshPartTriIndexOffsets;
private int[] meshPartTriCounts;
private int numDisconnectedEdges;
private int numConnectedEdges;
private int numTotalEdges;
public NavMeshGraph(Model model) {
short[] indices = getUniquePositionVertexIndices(model.meshes.first());
Array<IndexConnection> indexConnections = getIndexConnections(indices);
Vector3[] vertexVectors = createVertexVectors(model.meshes.first(), indices);
// The triangle graph uses consecutive indices for each unique triangle in the whole model.
// The Bullet raycast uses MeshPart index combined with triangle index inside the MeshPart,
// so we need to be able to convert between them.
int[] meshPartIndexOffsets = new int[model.meshParts.size];
meshPartTriIndexOffsets = new int[model.meshParts.size];
for (int i = 0; i < model.meshParts.size; i++) {
MeshPart meshPart = model.meshParts.get(i);
meshPartIndexOffsets[i] = meshPart.offset;
meshPartTriIndexOffsets[i] = meshPart.offset / 3;
}
meshPartTriCounts = new int[model.meshParts.size];
Array<Triangle> triangles = createTriangles(vertexVectors, indices,
meshPartIndexOffsets, meshPartTriCounts);
sharedEdges = createSharedEdgesMap(indexConnections, triangles, vertexVectors);
isolatedEdgesMap = createIsolatedEdgesMap(sharedEdges);
// Count edges of different types
for (Array<Edge> edges : isolatedEdgesMap.values()) {
numDisconnectedEdges += edges.size;
}
for (Array<Edge> edges : sharedEdges.values()) {
numConnectedEdges += edges.size;
}
numConnectedEdges /= 2;
numTotalEdges = numConnectedEdges + numDisconnectedEdges;
Gdx.app.debug(TAG, "MeshParts: total=" + getMeshPartCount() + ", Triangles: total=" + getNodeCount()
+ ", Edges: connected=" + getEdgeCountShared() + ", disconnected=" + getEdgeCountIsolated() + ", total=%s"
+ getEdgeCountTotal());
}
/**
* Map the isolated edges for each triangle which does not have all three edges connected to other triangles.
*
* @param connectionMap
* @return
*/
private static ArrayMap<Triangle, Array<Edge>> createIsolatedEdgesMap(ArrayMap<Triangle, Array<Edge>> connectionMap) {
ArrayMap<Triangle, Array<Edge>> disconnectionMap = new ArrayMap<Triangle, Array<Edge>>();
for (int i = 0; i < connectionMap.size; i++) {
Triangle tri = connectionMap.getKeyAt(i);
Array<Edge> connectedEdges = connectionMap.getValueAt(i);
Array<Edge> disconnectedEdges = new Array<Edge>();
disconnectionMap.put(tri, disconnectedEdges);
if (connectedEdges.size < 3) {
// This triangle does not have all edges connected to other triangles
boolean ab = true;
boolean bc = true;
boolean ca = true;
for (Edge edge : connectedEdges) {
if (edge.rightVertex == tri.a && edge.leftVertex == tri.b) ab = false;
else if (edge.rightVertex == tri.b && edge.leftVertex == tri.c) bc = false;
else if (edge.rightVertex == tri.c && edge.leftVertex == tri.a) ca = false;
}
if (ab) disconnectedEdges.add(new Edge(tri, null, tri.a, tri.b));
if (bc) disconnectedEdges.add(new Edge(tri, null, tri.b, tri.c));
if (ca) disconnectedEdges.add(new Edge(tri, null, tri.c, tri.a));
}
int totalEdges = (connectedEdges.size + disconnectedEdges.size);
if (totalEdges != 3) {
Gdx.app.debug(TAG, "Wrong number of edges (" + totalEdges + ") in triangle " + tri.getIndex());
}
}
return disconnectionMap;
}
/**
* Creates a map over each triangle and its Edge connections to other triangles. Each edge must follow the
* vertex winding order of the triangle associated with it. Since all triangles are assumed to have the same
* winding order, this means if two triangles connect, each must have its own edge connection data, where the
* edge follows the same winding order as the triangle which owns the edge data.
*
* @param indexConnections
* @param triangles
* @param vertexVectors
* @return
*/
private static ArrayMap<Triangle, Array<Edge>> createSharedEdgesMap(
Array<IndexConnection> indexConnections, Array<Triangle> triangles, Vector3[] vertexVectors) {
ArrayMap<Triangle, Array<Edge>> connectionMap = new ArrayMap<Triangle, Array<Edge>>();
connectionMap.ordered = true;
for (Triangle tri : triangles) {
connectionMap.put(tri, new Array<Edge>());
}
for (IndexConnection i : indexConnections) {
Triangle fromNode = triangles.get(i.fromTriIndex);
Triangle toNode = triangles.get(i.toTriIndex);
Vector3 edgeVertexA = vertexVectors[i.edgeVertexIndex1];
Vector3 edgeVertexB = vertexVectors[i.edgeVertexIndex2];
Edge edge = new Edge(fromNode, toNode, edgeVertexA, edgeVertexB);
connectionMap.get(fromNode).add(edge);
fromNode.connections.add(edge);
}
return connectionMap;
}
/**
* Get an array of the vertex indices from the mesh. Any vertices which share the same position will be counted
* as a single vertex and share the same index. That is, position duplicates will be filtered out.
*
* @param mesh
* @return
*/
private static short[] getUniquePositionVertexIndices(Mesh mesh) {
FloatBuffer verticesBuffer = mesh.getVerticesBuffer();
int positionOffset = mesh.getVertexAttributes().findByUsage(VertexAttributes.Usage.Position).offset / 4;
// Number of array elements which make up a vertex
int vertexSize = mesh.getVertexSize() / 4;
// The indices tell us which vertices are part of a triangle.
short[] indices = new short[mesh.getNumIndices()];
mesh.getIndices(indices);
// Marks true if an index has already been compared to avoid unnecessary comparisons
Bits handledIndices = new Bits(mesh.getNumIndices());
for (int i = 0; i < indices.length; i++) {
short indexI = indices[i];
if (handledIndices.get(indexI)) {
// Index handled in an earlier iteration
continue;
}
int vBufIndexI = indexI * vertexSize + positionOffset;
float xi = verticesBuffer.get(vBufIndexI++);
float yi = verticesBuffer.get(vBufIndexI++);
float zi = verticesBuffer.get(vBufIndexI++);
for (int j = i + 1; j < indices.length; j++) {
short indexJ = indices[j];
int vBufIndexJ = indexJ * vertexSize + positionOffset;
float xj = verticesBuffer.get(vBufIndexJ++);
float yj = verticesBuffer.get(vBufIndexJ++);
float zj = verticesBuffer.get(vBufIndexJ++);
if (xi == xj && yi == yj && zi == zj) {
indices[j] = indexI;
}
}
handledIndices.set(indexI);
}
return indices;
}
/**
* Creates triangle objects according to the index array, using Vector3 objects from the provided vector array.
*
* @param vertexVectors
* @param indices
* @param meshPartIndexOffsets
* @param meshPartTriCounts
* @return
*/
private static Array<Triangle> createTriangles(Vector3[] vertexVectors, short[] indices,
int[] meshPartIndexOffsets, int[] meshPartTriCounts) {
Array<Triangle> triangles = new Array<Triangle>();
triangles.ordered = true;
short i = 0;
short j = 0;
int triIndex = 0;
int meshPartIndex = -1;
while (i < indices.length) {
if (j < meshPartIndexOffsets.length && i >= meshPartIndexOffsets[j]) {
meshPartIndex++;
j++;
}
triangles.add(new Triangle(
vertexVectors[indices[i++]],
vertexVectors[indices[i++]],
vertexVectors[indices[i++]],
triIndex, meshPartIndex));
meshPartTriCounts[meshPartIndex]++;
triIndex++;
}
return triangles;
}
/**
* Creates Vector3 objects from the vertices of the mesh. The resulting array follows the ordering of the provided
* index array.
*
* @param mesh
* @param indices
* @return
*/
private static Vector3[] createVertexVectors(Mesh mesh, short[] indices) {
FloatBuffer verticesBuffer = mesh.getVerticesBuffer();
int positionOffset = mesh.getVertexAttributes().findByUsage(VertexAttributes.Usage.Position).offset / 4;
int vertexSize = mesh.getVertexSize() / 4;
Vector3[] vertexVectors = new Vector3[mesh.getNumIndices()];
for (int i = 0; i < indices.length; i++) {
short index = indices[i];
int a = index * vertexSize + positionOffset;
float x = verticesBuffer.get(a++);
float y = verticesBuffer.get(a++);
float z = verticesBuffer.get(a);
vertexVectors[index] = new Vector3(x, y, z);
}
return vertexVectors;
}
/**
* Loops through each triangle among the indices and searches for edges shared with other triangles.
* TODO: Can be optimized by removing triangles which have all their sides connected at some point.
*
* @param indices
* @return
*/
private static Array<IndexConnection> getIndexConnections(short[] indices) {
Array<IndexConnection> indexConnections = new Array<IndexConnection>();
indexConnections.ordered = true;
short[] edge = {-1, -1};
short i = 0;
short j, a0, a1, a2, b0, b1, b2, triAIndex, triBIndex;
while (i < indices.length) {
triAIndex = (short) (i / 3);
a0 = indices[i++];
a1 = indices[i++];
a2 = indices[i++];
j = i;
while (j < indices.length) {
triBIndex = (short) (j / 3);
b0 = indices[j++];
b1 = indices[j++];
b2 = indices[j++];
if (hasSharedEdgeIndices(a0, a1, a2, b0, b1, b2, edge)) {
indexConnections.add(new IndexConnection(edge[0], edge[1], triAIndex, triBIndex));
indexConnections.add(new IndexConnection(edge[1], edge[0], triBIndex, triAIndex));
edge[0] = -1;
edge[1] = -1;
}
}
}
return indexConnections;
}
/**
* Checks if the two triangles have shared vertex indices. The edge will always follow the vertex winding order
* of the triangle A. Since all triangles have the same winding order, triangle A should have the opposite
* edge direction to triangle B.
*
* @param a0 Vertex index on triangle A
* @param a1
* @param a2
* @param b0 Vertex index on triangle B
* @param b1
* @param b2
* @param edge Output, the indices of the shared vertices in the winding order of triangle A.
* @return True if the triangles share an edge.
*/
private static boolean hasSharedEdgeIndices(short a0, short a1, short a2,
short b0, short b1, short b2, short[] edge) {
boolean match0 = (a0 == b0 || a0 == b1 || a0 == b2);
boolean match1 = (a1 == b0 || a1 == b1 || a1 == b2);
if (!match0 && !match1) {
return false;
} else if (match0 && match1) {
edge[0] = a0;
edge[1] = a1;
return true;
}
boolean match2 = (a2 == b0 || a2 == b1 || a2 == b2);
if (match0 && match2) {
edge[0] = a2;
edge[1] = a0;
return true;
} else if (match1 && match2) {
edge[0] = a1;
edge[1] = a2;
return true;
}
return false;
}
@Override
public int getNodeCount() {
return sharedEdges.size;
}
/**
* The number of triangles in specified MeshPart.
*
* @param meshPartIndex
* @return
*/
public int getTriangleCount(int meshPartIndex) {
return meshPartTriCounts[meshPartIndex];
}
@Override
public int getIndex (Triangle node) {
return node.getIndex();
}
@Override
@SuppressWarnings("unchecked")
public Array<Connection<Triangle>> getConnections(Triangle fromNode) {
return (Array<Connection<Triangle>>) (Array<?>) sharedEdges.getValueAt(fromNode.triIndex);
}
/**
* Get triangle edges which do not connect to another triangle.
*
* @param triIndex
* @return
*/
public Array<Edge> getIsolatedEdges(int triIndex) {
return isolatedEdgesMap.getValueAt(triIndex);
}
/**
*
* Get triangle edges which are connected to two triangles
*
* @param triIndex
* @return
*/
public Array<Edge> getSharedEdges(int triIndex) {
return sharedEdges.getValueAt(triIndex);
}
/**
* Get a triangle using its index in the pathfinding graph.
*
* @param graphTriIndex
* @return
*/
public Triangle getTriangleFromGraphIndex(int graphTriIndex) {
return sharedEdges.getKeyAt(graphTriIndex);
}
/**
* Get a triangle using the index of a MeshPart and triangle index inside the Meshpart.
*
* @param meshPartIndex
* @param meshPartTriIndex
* @return
*/
public Triangle getTriangleFromMeshPart(int meshPartIndex, int meshPartTriIndex) {
return sharedEdges.getKeyAt(meshPartTriIndexOffsets[meshPartIndex] + meshPartTriIndex);
}
/**
* The number of MeshParts in the navigation mesh.
*
* @return
*/
public int getMeshPartCount() {
return meshPartTriIndexOffsets.length;
}
/**
* The total number of edges in the navigation mesh.
*
* @return
*/
public int getEdgeCountTotal() {
return numTotalEdges;
}
/**
* The number of (unique) edges which are connected to two triangles.
*
* @return
*/
public int getEdgeCountShared() {
return numConnectedEdges;
}
/**
* The number of edges which are only connected to one triangle.
*
* @return
*/
public int getEdgeCountIsolated() {
return numDisconnectedEdges;
}
}