先看效果,下面两张图,第一张是unity烘焙好的网格,第二张是导出的文件,第二张因为数字长和宽不相等,所以形状被压缩了下。
一、实现
1、这个方法做的功能就是将navmesh网格的顶点和顶点索引导出并封装成方便操作的数据。
private void LoadNavMeshToArray()
{
NavMeshTriangulation tmpNavMeshTriangulation = NavMesh.CalculateTriangulation();
//将每个三角形的顶点索引分组放到二维数组里
int[,] convexs = new int[tmpNavMeshTriangulation.areas.Length,3];
for (int i = 0, j = 0; i < tmpNavMeshTriangulation.indices.Length; j++)
{
convexs[j, 0] = tmpNavMeshTriangulation.indices[i];
convexs[j, 1] = tmpNavMeshTriangulation.indices[i + 1];
convexs[j, 2] = tmpNavMeshTriangulation.indices[i + 2];
i = i + 3;
}
navPath = new int[mapSize.x, mapSize.y];
//将地图二维数组、三角形索引、顶点传入方法进行绘图
ConvexsTraverse(navPath, convexs, tmpNavMeshTriangulation.vertices);//用索引值遍历三角形
state = 1;
Repaint();
SceneView.RepaintAll();
}
2、根据索引和顶点遍历所有三角形
public void ConvexsTraverse(int[,] navPath, int[,] convexs, Vector3[] vertexs)//用索引值遍历三角形
{
Vector3[] vertex3 = new Vector3[3];
for (int i = 0; i < (convexs.Length / 3); i++)//每次处理一个三角形
{
vertex3[0] = vertexs[convexs[i, 0]];
vertex3[1] = vertexs[convexs[i, 1]];
vertex3[2] = vertexs[convexs[i, 2]];
VertexsTraverse(navPath, vertex3);
FillDifference(navPath, vertex3);
}
}
3、代码量有点多就大概说一下用了什么算法,然后直接把完整代码全部贴出来吧。
用DDA直线生成算法画边框,三角型用扫描线填充法,三角形分上下三角型进行填充,不规则三角形就分割成上下三角形。
最后一个多边形扫描填充算法没用上哈也贴出来供参考。
另外,其实用DDA直线生成算法锯齿较大哈,可以选用Bresenham直线算法锯齿会小很多,然后填充的画其实也可以选用边界填充或泛滥填充更好。
using NUnit.Framework.Internal;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.UI;
namespace Assets.Editor.SceneEditor
{
public class EditorNavMesh : EditorWindow
{
#region Singleton
private static EditorNavMesh m_Instance = null;
public static EditorNavMesh Instance
{
get { return m_Instance; }
}
#endregion
private const string CLIENT_DATA_PATH = "";//PathDefine.EnvDataPath + "Data/config/csv";
private const string NAVMESH_MESHS_PATH = "";//PathDefine.EnvConfigPath + "navmesh/meshs";
GameObject parent;//测试使用
private int[,] navPath;
private string sSavePath;
private string sNavFileName;
public int state = 0;
public Vector2Int mapSize = new Vector2Int(50,50);
public int _id = 1;
public static void Init()
{
if (m_Instance == null)
{
m_Instance = EditorWindow.GetWindow<EditorNavMesh>();
m_Instance.autoRepaintOnSceneChange = true;
}
}
public void Awake()
{
title = "EditorNavMeshByArray";
SceneView.onSceneGUIDelegate += OnSceneView;
}
private void OnDestroy()
{
SceneView.onSceneGUIDelegate -= OnSceneView;
DeleteTest();
}
public int clones = 1;
private void OnGUI()
{
EditorGUILayout.BeginHorizontal();
mapSize = EditorGUILayout.Vector2IntField("MapSize:", mapSize);
EditorGUILayout.EndHorizontal();
if(state >= 0)//加载了地图才显示这个按钮
{
EditorGUILayout.BeginVertical();
if (GUILayout.Button("LoadNavMeshToArray"))
{
LoadNavMeshToArray();
}
EditorGUILayout.EndVertical();
}
if (state > 0)//加载了nav才显示保存按钮
{
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
_id = EditorGUILayout.IntField(_id);
if (GUILayout.Button("Save"))
{
Save(_id);
}
if (GUILayout.Button("Test"))
{
TestNavArray();
}
if (GUILayout.Button("DeleteTest"))
{
DeleteTest();
}
EditorGUILayout.EndHorizontal();
GUILayout.Label("SavePath:" + sSavePath);
EditorGUILayout.EndVertical();
}
Repaint();
SceneView.RepaintAll();
}
private void OnSceneView(SceneView view)
{
Repaint();
}
private void DeleteTest()
{
if (parent != null)
GameObject.DestroyImmediate(parent);
parent = null;
}
private void TestNavArray()//用方块来测试
{
DeleteTest();
parent = GameObject.CreatePrimitive(PrimitiveType.Cube);
parent.transform.position = new Vector3(0, 0, 0);
parent.name = "parent";
for (int i = 0; i < mapSize.x ; i++)
{
for(int j = 0; j < mapSize.y; j++)
{
if(navPath[i,j] == 1)
{
GameObject obj1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj1.transform.position = new Vector3(j, 1, i);
obj1.transform.localScale = new Vector3(0.9f, 0.9f, 0.9f);
obj1.name = i.ToString() + j.ToString();
obj1.transform.parent = parent.transform;
}
}
}
}
private void Save(int id)
{
string path = EditorUtility.SaveFilePanel("Save nav", Application.dataPath, sNavFileName, "xml");
sSavePath = path;
WriteMapDataByXml(id, navPath, mapSize.x, mapSize.y);
}
private void LoadNavMeshToArray()
{
NavMeshTriangulation tmpNavMeshTriangulation = NavMesh.CalculateTriangulation();
int[,] convexs = new int[tmpNavMeshTriangulation.areas.Length,3];
for (int i = 0, j = 0; i < tmpNavMeshTriangulation.indices.Length; j++)
{
convexs[j, 0] = tmpNavMeshTriangulation.indices[i];
convexs[j, 1] = tmpNavMeshTriangulation.indices[i + 1];
convexs[j, 2] = tmpNavMeshTriangulation.indices[i + 2];
i = i + 3;
}
navPath = new int[mapSize.x, mapSize.y];
ConvexsTraverse(navPath, convexs, tmpNavMeshTriangulation.vertices);//用索引值遍历三角形
state = 1;
Repaint();
SceneView.RepaintAll();
}
[MenuItem("NavmeshEditor/NavmeshToXmlByArr")]
public static void ExportNavMeshToXml()
{
Init();
}
public void ConvexsTraverse(int[,] navPath, int[,] convexs, Vector3[] vertexs)//用索引值遍历三角形
{
Vector3[] vertex3 = new Vector3[3];
for (int i = 0; i < (convexs.Length / 3); i++)//每次处理一个三角形
{
vertex3[0] = vertexs[convexs[i, 0]];
vertex3[1] = vertexs[convexs[i, 1]];
vertex3[2] = vertexs[convexs[i, 2]];
VertexsTraverse(navPath, vertex3);
FillDifference(navPath, vertex3);
}
}
public void VertexsTraverse(int[,] navPath, Vector3[] vertexs)//顶点遍历
{
Vector3 v1, v2;
int nSize = vertexs.Length;
for (int i = 0; i < nSize; i++)
{
v1 = vertexs[i % nSize];
v2 = vertexs[(i + 1) % nSize];
ConstructLine(navPath, v1, v2);
}
}
//将转换的地图导出xml
public void WriteMapDataByXml(int id, int[,] navPath, int row, int col)
{
StreamWriter writer;
StringBuilder message = new StringBuilder();
FileInfo file = new FileInfo(sSavePath);
writer = file.CreateText();
String data = @"<root type='MapMask'>";
message.Append(data);
message.Append("\n");
message.Append(string.Format("<id>{0}</id>\n", id));
message.Append(string.Format("<width>{0}</width>\n", col));
message.Append(string.Format("<height>{0}</height>\n", row));
message.Append("<mask>\n");
for (int i = col - 1; i >= 0; i--)
{
for (int j = 0; j < row; j++)
{
message.Append(navPath[i, j]);
}
message.Append("\n");
}
message.Append("</mask>\n");
message.Append("</root>");
writer.Write(message);
writer.Flush();
writer.Dispose();
writer.Close();
}
//根据顶点绘制边框算法,两点差值绘制法
public void ConstructLine(int[,] navPath, Vector3 v1, Vector3 v2)
{
float nxd, nzd;//x和z轴相互间距离
float mod, normalX, normalZ;
float nx, nz;//用于存差值计算的点
float nSafaX = 0, nSafaZ = 0;//维护xz,用于边界过线检测
nxd = Mathf.Abs(v1.x - v2.x);
nzd = Mathf.Abs(v1.z - v2.z);
mod = Mathf.Sqrt(nxd * nxd + nzd * nzd);
normalX = nxd / mod;
normalZ = nzd / mod;
nx = v1.x;
nz = v1.z;
navPath[(int)nz, (int)nx] = 1;
if (v1.z == v2.z)//一横
{
if (v1.x < v2.x)
{
while (nx <= v2.x)//末尾压线
{
navPath[(int)nz, (int)nx] = 1;
nx = nx + normalX;
}
nSafaX = (v2.x - (int)v2.x) > 0 ? v2.x + 1 : v2.x;//过线维护
nSafaZ = nz;
}
else
{
while (nx >= v2.x)
{
navPath[(int)nz, (int)nx] = 1;
nx = nx - normalX;
}
nSafaX = v2.x;//过线维护
nSafaZ = nz;
}
}
else if (v1.x == v2.x)//一竖
{
if (v1.z < v2.z)
{
while (nz <= v2.z)
{
navPath[(int)nz, (int)nx] = 1;
nz = nz + normalZ;
}
nSafaX = nx;//过线维护
nSafaZ = (v2.z - (int)v2.z) > 0 ? v2.z + 1 : v2.z;
}
else
{
while (nz >= v2.z)
{
navPath[(int)nz, (int)nx] = 1;
nz = nz - normalZ;
}
nSafaX = nx;//过线维护
nSafaZ = v2.z;
}
}
else if (v1.x < v2.x && v1.z > v2.z)//右上撇,1x小于2x,1z大于2z情况
{
while (nx <= v2.x)
{
navPath[(int)nz, (int)nx] = 1;
nx = nx + normalX;
nz = nz - normalZ;
}
nSafaX = (v2.x - (int)v2.x) > 0 ? v2.x + 1 : v2.x;//过线维护
nSafaZ = v2.z;
}
else if (v1.x < v2.x && v1.z < v2.z)//右下奈,1xz小于2xz情况
{
while (nx <= v2.x)
{
navPath[(int)nz, (int)nx] = 1;
nx = nx + normalX;
nz = nz + normalZ;
}
nSafaX = (v2.x - (int)v2.x) > 0 ? v2.x + 1 : v2.x;//过线维护
nSafaZ = (v2.z - (int)v2.z) > 0 ? v2.z + 1 : v2.z;
}
else if (v1.x > v2.x && v1.z < v2.z)//左下撇,1x大于2x,1z小于2z情况
{
while (nx >= v2.x)
{
navPath[(int)nz, (int)nx] = 1;
nx = nx - normalX;
nz = nz + normalZ;
}
nSafaX = v2.x;//过线维护
nSafaZ = (v2.z - (int)v2.z) > 0 ? v2.z + 1 : v2.z;
}
else if (v1.x > v2.x && v1.z > v2.z)//左上奈,1xz大于于2xz情况
{
while (nx >= v2.x)
{
navPath[(int)nz, (int)nx] = 1;
nx = nx - normalX;
nz = nz - normalZ;
}
nSafaX = v2.x;//过线维护
nSafaZ = v2.z;
}
//navPath[(int)nSafaZ, (int)nSafaX] = 1;
}
//差值填充三角形算法
public void FillDifference(int[,] navPath, Vector3[] vertexs)
{
int i;
int len = vertexs.Length;
Vector3 v1, v2, v3;
for (i = 0; i < len; i++)//处理简单的三角形
{
v1 = vertexs[i % len];
v2 = vertexs[(i + 1) % len];
if (v1.z == v2.z)
{
if (v1.x <= v2.x)
FillSimpleTriangle(navPath, v1, v2, vertexs[(i + 2) % len]);
else
FillSimpleTriangle(navPath, v2, v1, vertexs[(i + 2) % len]);
return;
}
}
//不是简单三角形就分上下两部分进行处理
for (i = 0; i < len; i++)//顺时针旋转判断
{
v1 = vertexs[i % len];
v2 = vertexs[(i + 1) % len];
v3 = vertexs[(i + 2) % len];
if (v2.z < v1.z && v1.z < v3.z)
{
FillComplexTriangle(navPath, v1, v2, v3);
return;
}
}
for (i = 0; i < len; i++)//逆时针旋转判断
{
v1 = vertexs[i % len];
v2 = vertexs[(i + 2) % len];
v3 = vertexs[(i + 1) % len];
if (v2.z < v1.z && v1.z < v3.z)
{
FillComplexTriangle(navPath, v1, v2, v3);
return;
}
}
}
//填充简单三角形,左右顶点,上边或下边顶点
public void FillSimpleTriangle(int[,] navPath, Vector3 vl, Vector3 vr, Vector3 vc)
{
float nLxd, nRxd;//两个与不平行的点x距离
float nzd;//高度的平方
float lMod, rMod, lNormalX, rNormalX, normalZ;
float nLx, nRx;//用于存差值计算的点
nLxd = vc.x - vl.x;
nRxd = vc.x - vr.x;
nzd = Mathf.Abs(vc.z - vl.z);
lMod = Mathf.Sqrt(nLxd * nLxd + nzd * nzd);
rMod = Mathf.Sqrt(nRxd * nRxd + nzd * nzd);
//lNormalX = ((nLxd / lMod) * (lMod / nzd));
//rNormalX = ((nRxd / rMod) * (rMod / nzd));
lNormalX = nLxd / nzd;
rNormalX = nRxd / nzd;
//normalZ = 1;
int rowb, rowe;
int ndir = 0;//方向,1表示下,-1表示上
if (vl.z > vc.z)//上三角情况
{
rowb = (int)vl.z;
rowe = (int)vc.z;
ndir = -1;
}
else//下三角情况
{
rowb = (int)vl.z;
rowe = (int)vc.z;
ndir = 1;
}
nLx = vl.x;
nRx = vr.x;
for (rowb = rowb; rowb != rowe; rowb += ndir)
{
for (int col = (int)nLx; col <= nRx; col++)
{
//rowb = rowb > 0 ? rowb : 0;
//col = col > 0 ? col : 0;
navPath[rowb, col] = 1;
}
if ((nRx - (int)nRx) > 0)//过线维护
{
navPath[rowb, (int)nRx + 1] = 1;
}
nLx += lNormalX;
nRx += rNormalX;
}
}
//填充复杂三角形
public void FillComplexTriangle(int[,] navPath, Vector3 vl, Vector3 vt, Vector3 vb)
{
float nRxd, nRzd;
float rMod, rNormalX;
float nzd;//高度的平方
nRxd = vt.x - vb.x;
nRzd = vb.z - vt.z;
nzd = Mathf.Abs(vt.z - vb.z);
rMod = Mathf.Sqrt(nRxd * nRxd + nRzd * nRzd);
//rNormalX = ((nRxd / rMod) * (rMod / nzd));
rNormalX = nRxd / nzd;
float rowb, rowe;
float nBx = vb.x;//用于存差值计算的点
rowb = vb.z;
rowe = vl.z;
for (rowb = rowb; rowb >= rowe; rowb--)//找到分割点
{
nBx += rNormalX;
}
Vector3 vSplitPoint = new Vector3() { x = nBx, y = 0, z = rowe };//分割点
Vector3 vParallelPoint = vl;//与分割点平行的点
Vector3 vTopPoint = vt;//最上面的点
Vector3 vBottomPoint = vb;//最下面的点
if (vParallelPoint.x <= vSplitPoint.x)
{
FillSimpleTriangle(navPath, vParallelPoint, vSplitPoint, vTopPoint);//填充上半部分
FillSimpleTriangle(navPath, vParallelPoint, vSplitPoint, vBottomPoint);//填充下半部分
}
else
{
FillSimpleTriangle(navPath, vSplitPoint, vParallelPoint, vTopPoint);
FillSimpleTriangle(navPath, vSplitPoint, vParallelPoint, vBottomPoint);
}
}
//根据多边形填充算法,扫描线法(只对一个多边形正常)
public void FillPolygon(int[,] navPath, int row, int col)
{
int state = 0;//0表示离开,1填充,2进入
for (int i = 0; i < row; i++)
{
state = 0;
for (int j = 0; j < col; j++)//检测是否只有入口没有出口,是本行不填充
{
var cell = navPath[i, j];
if (cell == 1 && state == 0)
{
state = 2;
}
else if (cell == 0 && state == 2)
{
state = 1;
}
else if (cell == 1 && state == 1)
{
state = 0;
break;
}
}
if (state != 0)//如果不等于0就等于没有出口所以本行不用填充
continue;
for (int j = 0; j < col - 1; j++)//开始填充本行
{
if (navPath[i, j] == 1 && state == 0)
{
state = 2;
}
else if (navPath[i, j] == 0 && state == 2)
{
state = 1;
}
else if (navPath[i, j] == 1 && state == 1)
{
if (navPath[i, (j + 1)] == 0)
state = 0;
}
if (state == 1)
{
navPath[i, j] = 1;
}
}
}
}
}
}
二、使用
1、赋值上面代码创建脚本粘贴进去
2、MapSize是地图大小,大小必须超过网格的最大值
2、Test按钮可以帮助你测试看看生成的数据怎样的,如下图