在Unity中,如果将一个模型文件(比如从max里面导出一个fbx文件)导入到编辑器中之后,Unity会把所有在原来在面列表中公用的顶点复制一份,保证每个三角形使用的顶点都是单独的,不与其它三角形共用顶点,Unityh这么做应该有他的道理,但我尚未想明白Unity这么做的原因是什么。
但是考虑到在有些应用中使用共用顶点能够更好的处理一些问题,这里写了一个将Unity中Mesh重叠顶点合并成公用顶点的代码,目的是将位置重叠的顶点去掉,同时也将面列表的内容更新,由一个原来有重叠顶点的Mesh获得一个新的使用公用顶点的Mesh,参考如下:
cs
Mesh GetWeldMesh(Mesh mesh)
{
Vector3[] vs = mesh.vertices;
int[] ts = mesh.triangles;
Debug.Log(ts.Length);
Dictionary<Vector3, int> dicVert = new();
for (int i = 0; i < ts.Length; i++)
{
bool hasSamePos = false;
Vector3 vert = vs[ts[i]];
foreach (var kv in dicVert)
{
Vector3 key = kv.Key;
if (vert == key)
{
if (kv.Value < ts[i])
{
ts[i] = kv.Value;
}
else
{
dicVert[kv.Key] = ts[i];
}
hasSamePos = true;
break;
}
}
if (!hasSamePos)
{
dicVert.Add(vert, ts[i]);
}
}
List<Vector3> listVertice = new();
foreach (var kv in dicVert)
{
listVertice.Add(kv.Key);
}
Mesh meshNew = new()
{
vertices = vs,
triangles = ts
};
meshNew.RecalculateNormals();
return meshNew;
}
目前我能想到的这么做的用处有两个,第一个是在一种描边方法中,Shader需要将要描边的物体增加一次渲染,在这次渲染中,Shader会将网格的每个顶点沿着该顶点的法线方向向外挤出一点点,形成一个更大的轮廓,然后反面渲染,这样就形成了原物体的勾边效果,不过这里有个问题,如果使用的Unity默认的Mesh,当物体的光滑组是连续的时候,挤出的效果是没问题的,当光滑组不连续(例如一个Box),顶点挤出后会出现分离问题。你当然可以在类似max这种软件中将Box的光滑组强行编成一组,这样导入Untiy中之后挤出效果倒是没问题了,但是本身显示效果就不是硬边的效果了,这也不对啊。但是如果在使用Shader渲染Mesh的时候,使用的不是原始的Mesh,而是通过上述代码生成的新的Mesh,由于顶点是共用的,就不存在出现挤出分离的问题。关于具体的Shader就不说了,玩过Shader的都懂。
第二个是查找物体的边界,一般来说查找物体边界的方法是找到不被两个三角形共用的边,但是Unity中Mesh默认所有的顶点都是单独被面列表使用的,根本没有所谓被两个三角形共用的边的情况,这中情况会发现所有的三角形的边都是边界,貌似行不通,所以还是需要上述方法将Unity中Mesh重叠顶点合并再去查找边界。其查找边界的代码参考如下(由于Mesh的编辑可能不止一组,所以用的返回值是List<int[]>,是顶点编号数组的列表):
cs
List<int[]> GetEdgeIndexes(Mesh mesh)
{
Vector3[] vs = mesh.vertices;
int[] ts = mesh.triangles;
Dictionary<Edge, int> dicEdge = new();
for (int i = 0; i < ts.Length; i += 3)
{
int indexA = ts[i];
int indexB = ts[i + 1];
int indexC = ts[i + 2];
bool hasSameEdge01 = false;
bool hasSameEdge02 = false;
bool hasSameEdge03 = false;
KeyValuePair<Edge, int>[] kvs = dicEdge.ToArray();
for (int j = 0; j < kvs.Length; j++)
{
Edge edge = kvs[j].Key;
if (edge.IsSame(indexA, indexB)) { hasSameEdge01 = true; dicEdge[edge] += 1; continue; }
if (edge.IsSame(indexB, indexC)) { hasSameEdge02 = true; dicEdge[edge] += 1; continue; }
if (edge.IsSame(indexC, indexA)) { hasSameEdge03 = true; dicEdge[edge] += 1; continue; }
}
if (!hasSameEdge01) { dicEdge.Add(new Edge(indexA, indexB), 1); }
if (!hasSameEdge02) { dicEdge.Add(new Edge(indexB, indexC), 1); }
if (!hasSameEdge03) { dicEdge.Add(new Edge(indexC, indexA), 1); }
}
List<Edge> edges = new List<Edge>();
foreach (var kv in dicEdge)
{
if (kv.Value == 1)
{
edges.Add(kv.Key);
}
}
foreach (var item in edges)
{
Debug.Log(item.a + " , " + item.b);
}
List<int[]> listEdgeIndexes = new();
while (edges.Count > 0)
{
List<int> listTriangle = new()
{
edges[0].a,
edges[0].b
};
edges.RemoveAt(0);
while (true)
{
int indexLast = listTriangle[^1];
bool hasAddIndex = false;
for (int i = 0; i < edges.Count; i++)
{
Edge edge = edges[i];
if (indexLast == edge.a)
{
listTriangle.Add(edge.b);
hasAddIndex = true;
edges.RemoveAt(i);
break;
}
if (indexLast == edge.b)
{
listTriangle.Add(edge.a);
hasAddIndex = true;
edges.RemoveAt(i);
break;
}
}
if (!hasAddIndex)
{
listEdgeIndexes.Add(listTriangle.ToArray());
break;
}
}
}
return listEdgeIndexes;
}
其中所用到的Edge类代码如下:
cs
public class Edge
{
public readonly int a;
public readonly int b;
public Edge(int a, int b)
{
this.a = a;
this.b = b;
}
public bool IsSame(Edge edge) { return (a == edge.a && b == edge.b) || (a == edge.b && b == edge.a); }
public bool IsSame(int a, int b) { return (a == this.a && b == this.b) || (a == this.b && b == this.a); }
}
下图将一个球体挖了三个洞洞,通过上述方法查找到边界,并用LineRenderer渲染了边界。