2025-07-17 18:26:28 +08:00

755 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 从Unity的NavMesh组件里导出地图数据供服务器来使用
/// https://blog.csdn.net/huutu/article/details/52672505
/// </summary>
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<Vert> verts = new List<Vert>();
}
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<Vert> vertList = new List<Vert>();
private static List<Face> faceList = new List<Face>();
private static List<Pair> pairList = new List<Pair>();
private static Dictionary<Vert, Face> vertFaceDict = new Dictionary<Vert, Face>();
private static Dictionary<Vert, Dictionary<Vert, Pair>> vertPairDict = new Dictionary<Vert, Dictionary<Vert, Pair>>();
private static Dictionary<float, Dictionary<float, Vert>> pointVertDict = new Dictionary<float, Dictionary<float, Vert>>();
private static Dictionary<int, Vert> indexVertDict = new Dictionary<int, Vert>();
private static string outputClientFolder = "../RecastNavMesh/Meshes/";
private static string outputServerFolder = "../Config/RecastNavData/ExportedObj/";
#region
[MenuItem("ET/NavMesh/ExportSceneObj", false, ETMenuItemPriority.NavMesh)]
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<float, Vert>());
}
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<int>();
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<Vert, Pair>());
}
if (!vertPairDict.ContainsKey(secondVert))
{
vertPairDict.Add(secondVert, new Dictionary<Vert, Pair>());
}
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
/// <summary>
/// 将NavMesh里的所有物体导出成为RecastNavigation可以识别的Obj文件。July.11.2020. Liu Gang.
/// </summary>
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<string, ObjMaterial> materialList = PrepareFileWrite();
List<MeshFilter> 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<MeshFilter> m_Meshes = new List<MeshFilter>();
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<string, ObjMaterial> PrepareFileWrite()
{
Clear();
return new Dictionary<string, ObjMaterial>();
}
public static List<MeshFilter> Collect()
{
List<MeshFilter> meshes = new List<MeshFilter>();
// 确定场景内必须有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<MeshFilter>();
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<string, ObjMaterial> materialList)
{
Mesh m = mf.sharedMesh;
Material[] mats = mf.GetComponent<Renderer>().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<triangles.Length;i+=3) {
// sb.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n",
// triangles[i]+1, triangles[i+1]+1, triangles[i+2]+1));
// }
int[] triangles = m.GetTriangles(material);
for (int i = 0; i < triangles.Length; i += 3)
{
//Because we inverted the x-component, we also needed to alter the triangle winding.
sb.Append(string.Format("f {1}/{1}/{1} {0}/{0}/{0} {2}/{2}/{2}\n",
triangles[i] + 1 + vertexOffset, triangles[i + 1] + 1 + normalOffset, triangles[i + 2] + 1 + uvOffset));
}
}
vertexOffset += m.vertices.Length;
normalOffset += m.normals.Length;
uvOffset += m.uv.Length;
return sb.ToString();
}
#endregion
#region
/// <summary>
/// 把生成的Obj文件拷贝到服务器
/// https://www.cnblogs.com/wangjianhui008/p/3234519.html
/// </summary>
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
}
}