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
}
}