using System; using System.IO; using System.Collections.Generic; using System.Text; using ET; using UnityEditor; using UnityEditor.UI; using UnityEngine; using UnityEngine.AI; using UnityEngine.Networking; using UnityEngine.SceneManagement; namespace ETEditor { /// /// 从Unity的NavMesh组件里导出地图数据,供服务器来使用 /// https://blog.csdn.net/huutu/article/details/52672505 /// public class NavMeshExporter: Editor { public const byte VERSION = 1; private class Vert { public int id; public float x; public float y; public float z; public UnityEngine.Vector3 ToVector3() { return new UnityEngine.Vector3(x, y, z); } } private class Face { public int id; public int area; public float centerX; public float centerZ; public float normalX; public float normalZ; public double normalA; public double normalB; public double normalC; public double normalD; public uint sortValue; public List verts = new List(); } private class Pair { public float centerX; public float centerZ; public float distance; public Face firstEdgeFace; public int firstEdgeIndex; public Face secondEdgeFace; public int secondEdgeIndex; } private static List vertList = new List(); private static List faceList = new List(); private static List pairList = new List(); private static Dictionary vertFaceDict = new Dictionary(); private static Dictionary> vertPairDict = new Dictionary>(); private static Dictionary> pointVertDict = new Dictionary>(); private static Dictionary indexVertDict = new Dictionary(); private static string outputClientFolder = "../RecastNavMesh/Meshes/"; private static string outputServerFolder = "../Config/RecastNavData/ExportedObj/"; #region 菜单主函数 [MenuItem("ET/NavMesh/ExportSceneObj")] public static void ExportScene() { var triangulation = UnityEngine.AI.NavMesh.CalculateTriangulation(); if (triangulation.indices.Length < 3) { Debug.LogError($"NavMeshExporter ExportScene Error - 场景里没有需要被导出的物体,请先用NavMesh进行Bake。"); return; } vertList.Clear(); faceList.Clear(); pairList.Clear(); vertFaceDict.Clear(); vertPairDict.Clear(); pointVertDict.Clear(); indexVertDict.Clear(); InputVertices(triangulation.vertices); InputTriangles(triangulation.indices, triangulation.areas); IndexVertsAndFaces(); //WriteFile(); // 导出*_internal.Obj,仅供Unity编辑器自己查看 //WriteUnityObjFile(); // 导出Recast可用的*.Obj文件 WriteRecastObjFile(); // 拷贝Obj和Bytes文件到服务器目录下 TODO 暂不需要 //CopyObjFiles(); Debug.Log($"NavMesh Output Info - Vertices:[{vertList.Count}] - Faces:[{faceList.Count}]"); } #endregion #region 导出Bytes private static void InputVertices(Vector3[] vertices) { for (int i = 0, n = vertices.Length - 1; i <= n; i++) { var point = vertices[i]; var x = (float) Math.Round(point.x, 2); var y = (float) Math.Round(point.y, 2); var z = (float) Math.Round(point.z, 2); if (!pointVertDict.ContainsKey(x)) { pointVertDict.Add(x, new Dictionary()); } Vert vert; if (pointVertDict[x].ContainsKey(z)) { vert = pointVertDict[x][z]; } else { vert = new Vert(); vert.x = x; vert.y = y; vert.z = z; pointVertDict[x][z] = vert; } indexVertDict.Add(i, vert); } } private static void InputTriangles(int[] indices, int[] areas) { Face face = null; var faceIndices = new HashSet(); for (int i = 0, n = areas.Length; i < n; i++) { var triangleIndexList = new int[3]; var triangleVertList = new Vert[3]; for (var j = 0; j < 3; j++) { triangleIndexList[j] = indices[i * 3 + j]; triangleVertList[j] = indexVertDict[triangleIndexList[j]]; } var vert0 = triangleVertList[0]; var vert1 = triangleVertList[1]; var vert2 = triangleVertList[2]; if (vert0 == vert1 || vert1 == vert2 || vert2 == vert0) { continue; } var newFace = true; var area = areas[i] >= 3? areas[i] - 2 : 0; if (face != null && face.area == area) { for (var j = 0; j < 3; j++) { if (faceIndices.Contains(triangleIndexList[j])) { newFace = false; break; } } } if (newFace) { if (face != null) { InitFace(face); faceIndices.Clear(); } face = new Face(); face.area = area; } double x1 = vert1.x - vert0.x; double y1 = vert1.y - vert0.y; double z1 = vert1.z - vert0.z; double x2 = vert2.x - vert0.x; double y2 = vert2.y - vert0.y; double z2 = vert2.z - vert0.z; double normalA = y1 * z2 - z1 * y2; double normalB = z1 * x2 - x1 * z2; double normalC = x1 * y2 - y1 * x2; if (normalB < -0.000001 || 0.000001 < normalB) { var normalD = normalA + normalB + normalC; if (normalD > face.normalD) { face.normalA = normalA; face.normalB = normalB; face.normalC = normalC; face.normalD = normalD; } } for (var j = 0; j < 3; j++) { if (!faceIndices.Contains(triangleIndexList[j])) { faceIndices.Add(triangleIndexList[j]); face.verts.Add(triangleVertList[j]); } } } if (face != null) { InitFace(face); } foreach (var pair in pairList) { var firstFace = pair.firstEdgeFace; var secondFace = pair.secondEdgeFace; var firstDistance = GetDistance(firstFace.centerX - pair.centerX, firstFace.centerZ - pair.centerZ); var secondDistance = GetDistance(secondFace.centerX - pair.centerX, secondFace.centerZ - pair.centerZ); pair.distance = firstDistance + secondDistance; } } private static float GetDistance(float deltaX, float deltaZ) { return (float) Math.Round(Math.Sqrt((double) deltaX * (double) deltaX + (double) deltaZ * (double) deltaZ), 2); } private static void InitFace(Face face) { face.centerX = 0; face.centerZ = 0; var vertCount = face.verts.Count; foreach (var vert in face.verts) { face.centerX += vert.x; face.centerZ += vert.z; if (!vertFaceDict.ContainsKey(vert)) { vertFaceDict.Add(vert, face); vertList.Add(vert); } } face.centerX /= vertCount; face.centerZ /= vertCount; if (face.normalB != 0) { face.normalX = (float) Math.Round(face.normalA / face.normalB, 6); face.normalZ = (float) Math.Round(face.normalC / face.normalB, 6); } for (int i = 0, n = vertCount - 1; i <= n; i++) { var firstVert = face.verts[i]; var secondVert = face.verts[i == n? 0 : i + 1]; if (!vertPairDict.ContainsKey(firstVert)) { vertPairDict.Add(firstVert, new Dictionary()); } if (!vertPairDict.ContainsKey(secondVert)) { vertPairDict.Add(secondVert, new Dictionary()); } if (!vertPairDict[secondVert].ContainsKey(firstVert)) { var pair = new Pair(); pair.firstEdgeFace = face; pair.firstEdgeIndex = i; vertPairDict[firstVert][secondVert] = pair; } else { var pair = vertPairDict[secondVert][firstVert]; pair.centerX = (firstVert.x + secondVert.x) / 2; pair.centerZ = (firstVert.z + secondVert.z) / 2; pair.secondEdgeFace = face; pair.secondEdgeIndex = i; pairList.Add(pair); } } faceList.Add(face); } private static void IndexVertsAndFaces() { var minX = float.MaxValue; var maxX = float.MinValue; var minZ = float.MaxValue; var maxZ = float.MinValue; foreach (var vert in vertList) { if (minX > vert.x) { minX = vert.x; } if (maxX < vert.x) { maxX = vert.x; } if (minZ > vert.z) { minZ = vert.z; } if (maxZ < vert.x) { maxZ = vert.x; } } var hilbertX = 65535f / (maxX - minX); var hilbertZ = 65535f / (maxZ - minZ); foreach (var face in faceList) { var X = (uint) Math.Round((face.centerX - minX) * hilbertX); var Z = (uint) Math.Round((face.centerZ - minZ) * hilbertZ); var a = X ^ Z; var b = 0xFFFF ^ a; var c = 0xFFFF ^ (X | Z); var d = X & (Z ^ 0xFFFF); var A = a | (b >> 1); var B = (a >> 1) ^ a; var C = ((c >> 1) ^ (b & (d >> 1))) ^ c; var D = ((a & (c >> 1)) ^ (d >> 1)) ^ d; a = A; b = B; c = C; d = D; A = (a & (a >> 2)) ^ (b & (b >> 2)); B = (a & (b >> 2)) ^ (b & ((a ^ b) >> 2)); C ^= (a & (c >> 2)) ^ (b & (d >> 2)); D ^= (b & (c >> 2)) ^ ((a ^ b) & (d >> 2)); a = A; b = B; c = C; d = D; A = (a & (a >> 4)) ^ (b & (b >> 4)); B = (a & (b >> 4)) ^ (b & ((a ^ b) >> 4)); C ^= (a & (c >> 4)) ^ (b & (d >> 4)); D ^= (b & (c >> 4)) ^ ((a ^ b) & (d >> 4)); a = A; b = B; c = C; d = D; C ^= (a & (c >> 8)) ^ (b & (d >> 8)); D ^= (b & (c >> 8)) ^ ((a ^ b) & (d >> 8)); C ^= C >> 1; D ^= D >> 1; c = X ^ Z; d = D | (0xFFFF ^ (c | C)); c = (c | (c << 8)) & 0x00FF00FF; c = (c | (c << 4)) & 0x0F0F0F0F; c = (c | (c << 2)) & 0x33333333; c = (c | (c << 1)) & 0x55555555; d = (d | (d << 8)) & 0x00FF00FF; d = (d | (d << 4)) & 0x0F0F0F0F; d = (d | (d << 2)) & 0x33333333; d = (d | (d << 1)) & 0x55555555; face.sortValue = (d << 1) | c; } faceList.Sort(SortComparison); for (int i = 0, n = vertList.Count; i < n; i++) { vertList[i].id = i; } for (int i = 0, n = faceList.Count; i < n; i++) { faceList[i].id = i; } } private static int SortComparison(Face a, Face b) { return a.sortValue.CompareTo(b.sortValue); } private static void WriteFile() { if (!System.IO.Directory.Exists(outputClientFolder)) { System.IO.Directory.CreateDirectory(outputClientFolder); } var path = outputClientFolder + SceneManager.GetActiveScene().name + ".bytes"; var writer = new BinaryWriter(new FileStream(path, FileMode.Create)); writer.Write('N'); writer.Write('a'); writer.Write('v'); writer.Write('M'); writer.Write('e'); writer.Write('s'); writer.Write('h'); writer.Write(VERSION); writer.Write(vertList.Count); foreach (var vert in vertList) { writer.Write(vert.x); writer.Write(vert.y); writer.Write(vert.z); } writer.Write(faceList.Count); foreach (var face in faceList) { writer.Write(face.area); writer.Write(face.normalX); writer.Write(face.normalZ); writer.Write(face.verts.Count); foreach (var vert in face.verts) { writer.Write(vert.id); } } writer.Write(pairList.Count); foreach (var pair in pairList) { writer.Write(pair.distance); writer.Write(pair.firstEdgeFace.id); writer.Write(pair.firstEdgeIndex); writer.Write(pair.secondEdgeFace.id); writer.Write(pair.secondEdgeIndex); } writer.Flush(); writer.Close(); AssetDatabase.Refresh(); } #endregion #region 导出*_internal.Obj // ———————————————— // 版权声明:本文为CSDN博主「_Captain」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 // 原文链接:https://blog.csdn.net/huutu/article/details/52672505 private static void WriteUnityObjFile() { var path = outputClientFolder + SceneManager.GetActiveScene().name + "_internal.obj"; StreamWriter tmpStreamWriter = new StreamWriter(path); NavMeshTriangulation tmpNavMeshTriangulation = UnityEngine.AI.NavMesh.CalculateTriangulation(); //顶点 for (int i = 0; i < tmpNavMeshTriangulation.vertices.Length; i++) { tmpStreamWriter.WriteLine("v " + tmpNavMeshTriangulation.vertices[i].x + " " + tmpNavMeshTriangulation.vertices[i].y + " " + tmpNavMeshTriangulation.vertices[i].z); } tmpStreamWriter.WriteLine("g pPlane1"); //索引 for (int i = 0; i < tmpNavMeshTriangulation.indices.Length;) { tmpStreamWriter.WriteLine("f " + (tmpNavMeshTriangulation.indices[i] + 1) + " " + (tmpNavMeshTriangulation.indices[i + 1] + 1) + " " + (tmpNavMeshTriangulation.indices[i + 2] + 1)); i = i + 3; } tmpStreamWriter.Flush(); tmpStreamWriter.Close(); AssetDatabase.Refresh(); } #endregion #region 导出Obj(Recast使用) // ———————————————— // 版权声明:本文为CSDN博主「Rhett_Yuan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 // 原文链接:https://blog.csdn.net/rhett_yuan/article/details/79483387 // https://www.cnblogs.com/koshio0219/p/12195974.html // http://wiki.unity3d.com/index.php?title=ObjExporter#EditorObjExporter.cs /// /// 将NavMesh里的所有物体导出成为RecastNavigation可以识别的Obj文件。July.11.2020. Liu Gang. /// private static void WriteRecastObjFile() { if (!System.IO.Directory.Exists(outputClientFolder)) { System.IO.Directory.CreateDirectory(outputClientFolder); } var filename = SceneManager.GetActiveScene().name; var path = outputClientFolder + filename + ".obj"; StreamWriter sw = new StreamWriter(path); Dictionary materialList = PrepareFileWrite(); List meshes = Collect(); int count = 0; foreach (MeshFilter mf in meshes) { sw.Write("mtllib ./" + filename + ".mtl\n"); string strMes = MeshToString(mf, materialList); sw.Write(strMes); EditorUtility.DisplayProgressBar("Exporting objects...", mf.name, count++ / (float) meshes.Count); } sw.Flush(); sw.Close(); EditorUtility.ClearProgressBar(); AssetDatabase.Refresh(); } // Global containers for all active mesh/terrain tags public static List m_Meshes = new List(); private static string NAVMESH_TAG = "NavMesh"; private static int vertexOffset = 0; private static int normalOffset = 0; private static int uvOffset = 0; public struct ObjMaterial { public string name; public string textureName; } private static void Clear() { vertexOffset = 0; normalOffset = 0; uvOffset = 0; } private static Dictionary PrepareFileWrite() { Clear(); return new Dictionary(); } public static List Collect() { List meshes = new List(); // 确定场景内必须有NAVMESH_TAG这个tag // ———————————————— // 版权声明:本文为CSDN博主「懵懵爸爸」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 // 原文链接:https://blog.csdn.net/ljason1993/article/details/80924723 bool bFindTag = false; string[] strTags = UnityEditorInternal.InternalEditorUtility.tags; foreach (string tag in strTags) { if (tag == NAVMESH_TAG) { bFindTag = true; break; } } if (!bFindTag) { Debug.LogError($"NavMeshExporter Collect Error - 所有需要被NavMesh导出的物体的Tag必须是:[{NAVMESH_TAG}],目前的项目里没有这个Tag。"); return meshes; } MeshFilter[] meshFilters = FindObjectsOfType(); foreach (MeshFilter mf in meshFilters) { if (mf.gameObject.tag == NAVMESH_TAG) { meshes.Add(mf); } } if (meshes.Count == 0) { Debug.LogError($"NavMeshExporter Collect Error - 场景里没有需要被导出的物体,需要被导出的物体,它们的Tag必须是:[{NAVMESH_TAG}]。"); } return meshes; } public static string MeshToString(MeshFilter mf, Dictionary materialList) { Mesh m = mf.sharedMesh; Material[] mats = mf.GetComponent().sharedMaterials; StringBuilder sb = new StringBuilder(); sb.Append("g ").Append(mf.name).Append("\n"); // foreach(Vector3 v in m.vertices) { // sb.Append(string.Format("v {0} {1} {2}\n",v.x,v.y,v.z)); // } foreach (Vector3 lv in m.vertices) { Vector3 wv = mf.transform.TransformPoint(lv); //This is sort of ugly - inverting x-component since we're in //a different coordinate system than "everyone" is "used to". sb.Append(string.Format("v {0} {1} {2}\n", -wv.x, wv.y, wv.z)); } sb.Append("\n"); // foreach(Vector3 v in m.normals) { // sb.Append(string.Format("vn {0} {1} {2}\n",v.x,v.y,v.z)); // } foreach (Vector3 lv in m.normals) { Vector3 wv = mf.transform.TransformDirection(lv); sb.Append(string.Format("vn {0} {1} {2}\n", -wv.x, wv.y, wv.z)); } sb.Append("\n"); foreach (Vector3 v in m.uv) { sb.Append(string.Format("vt {0} {1}\n", v.x, v.y)); } int countMat = m.subMeshCount; if (mats == null) { Debug.LogWarning($"NavMeshExporter MeshToString Error - 没有找到材质"); return sb.ToString(); } else if (mats.Length < countMat) { Debug.LogWarning($"NavMeshExporter MeshToString Error - 共享材质数量小于该物体的子物体数量 - {mats.Length} / {countMat}"); countMat = mats.Length; } for (int material = 0; material < countMat; material++) { string nameMat = "null"; Texture mainTexture = null; if (mats[material] != null) { nameMat = mats[material].name; mainTexture = mats[material].mainTexture; } sb.Append("\n"); sb.Append("usemtl ").Append(nameMat).Append("\n"); sb.Append("usemap ").Append(nameMat).Append("\n"); //See if this material is already in the materiallist. try { ObjMaterial objMaterial = new ObjMaterial(); objMaterial.name = nameMat; if (mainTexture) objMaterial.textureName = AssetDatabase.GetAssetPath(mainTexture); else objMaterial.textureName = null; materialList.Add(objMaterial.name, objMaterial); } catch (ArgumentException) { //Already in the dictionary } // int[] triangles = m.GetTriangles(material); // for (int i=0;i /// 把生成的Obj文件拷贝到服务器 /// https://www.cnblogs.com/wangjianhui008/p/3234519.html /// private static void CopyObjFiles() { string sourceFolder = outputClientFolder; // *.bytes, *.obj, *_internal.obj文件不再拷贝到服务器的Config/Navmesh目录下,不再需要了,减少服务器数据文件的大小。Aug.27.2020. Liu Gang. //得到原文件根目录下的所有文件 // { // string[] files = System.IO.Directory.GetFiles(sourceFolder); // foreach (string file in files) // { // string name = System.IO.Path.GetFileName(file); // // 仅拷贝bytes文件和obj文件,但是不包括文件名里包含“internal”字样的obj文件。 // var ext = Path.GetExtension(file); // if (ext == ".bytes" || (ext == ".obj" && !file.Contains("_internal."))) // { // string dest = System.IO.Path.Combine(destFolder, name); // System.IO.File.Copy(file, dest, true); //复制文件 // } // } // } // 拷贝到RecastDemo配置路径 { string[] files = System.IO.Directory.GetFiles(sourceFolder); //如果目标路径不存在,则创建目标路径 if (!System.IO.Directory.Exists(outputServerFolder)) { System.IO.Directory.CreateDirectory(outputServerFolder); } foreach (string file in files) { string name = System.IO.Path.GetFileName(file); // 仅拷贝bytes文件和obj文件,但是不包括文件名里包含“internal”字样的obj文件。 var ext = Path.GetExtension(file); if (ext == ".obj" && !file.Contains("_internal.")) { string dest = System.IO.Path.Combine(outputServerFolder, name); System.IO.File.Copy(file, dest, true); //复制文件 UnityEngine.Debug.Log($"Recast:从{file}复制obj文件到{dest}成功"); } } } } #endregion } }