最近需要将一个2D网格边缘上绘制虚线,最初考虑使用渲染的后处理来实现,但是后处理代价大,而且效果也并不理想,于是打算采用LineRenderer来绘制边缘的虚线。
最后效果如下:
四张图分别是Quad、Plane、自定义形状、带空洞的自定义形状。
1、获取目标网格上的所有顶点、三角面信息。
2、提取出网格上所有三角面的三条边。
3、比较所边缘信息,清除重复使用的边缘。被共用的边缘是两个三角面的相交线,如Quad中间的那条对角线。
4、将为重复的边缘排序连成线,对有多边缘的形状,返回多条线。
5、使用LineRender组件将线绘制出来。
using System.Collections.Generic;
using UnityEngine;
public class MeshVertexLineRenderer : MonoBehaviour
{
public Material material;
public void AddLine()
{
AddLine(GetComponent().sharedMesh, transform);
}
public void DeleteLine()
{
DeleteLine(transform);
}
private void AddLine(Mesh mesh, Transform parent)
{
DeleteLine(parent);
Vector3[] meshVertices = mesh.vertices;
List> vertices = TrianglesAndVerticesEdge(mesh.vertices, mesh.triangles);
for (int i = 0; i < vertices.Count; i++)
AddSingleLine(i, vertices[i].ToArray(), parent);
}
private void AddSingleLine(int index, Vector3[] vertices, Transform parent)
{
LineRenderer lineRenderer = new GameObject("MeshVertexLine_" + index, new System.Type[] { typeof(LineRenderer) }).GetComponent();
lineRenderer.transform.parent = parent;
lineRenderer.transform.localPosition = Vector3.zero;
lineRenderer.transform.localRotation = Quaternion.identity;
lineRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
lineRenderer.receiveShadows = false;
lineRenderer.allowOcclusionWhenDynamic = false;
lineRenderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
lineRenderer.useWorldSpace = false;
lineRenderer.loop = true;
lineRenderer.widthMultiplier = 0.1f;
lineRenderer.sortingLayerName = "GamePlay";
lineRenderer.sortingOrder = 501;
lineRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
lineRenderer.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
lineRenderer.alignment = LineAlignment.View;
lineRenderer.textureMode = LineTextureMode.Tile;
lineRenderer.material = material;
lineRenderer.positionCount = vertices.Length;
lineRenderer.SetPositions(vertices);
}
private void DeleteLine(Transform parent)
{
for (int i = 0; i < parent.childCount; i++)
{
if (parent.GetChild(i).name.Contains("MeshVertexLine"))
{
GameObject gameObject = parent.GetChild(i).gameObject;
if (gameObject != null)
{
i--;
#if UNITY_EDITOR
if (Application.isPlaying)
{
Object.Destroy(gameObject);
}
else
{
Object.DestroyImmediate(gameObject);
}
#else
Object.Destroy(gameObject);
#endif
}
}
}
}
///
/// 网格系统边缘查找,支持多边缘
///
///
///
///
private List> TrianglesAndVerticesEdge(Vector3[] vertices, int[] triangles)
{
List edgeLines = TrianglesEdgeAnalysis(triangles);
List> result = SpliteLines(edgeLines, vertices);
return result;
}
///
/// 三角面组边缘提取
///
///
///
///
///
private List TrianglesEdgeAnalysis(int[] triangles)
{
int[,] edges = new int[triangles.Length, 2];
for (int i = 0; i < triangles.Length; i += 3)
{
for (int j = 0; j < 3; j++)
{
for (int k = 0; k < 2; k++)
{
int index = (j + k) % 3;
edges[i + j, k] = triangles[i + index];
}
}
}
bool[] invalidFlag = new bool[triangles.Length];
for (int i = 0; i < triangles.Length; i++)
{
for (int j = i + 1; j < triangles.Length; j++)
{
if ((edges[i, 0] == edges[j, 0] && edges[i, 1] == edges[j, 1]) || (edges[i, 0] == edges[j, 1] && edges[i, 1] == edges[j, 0]))
{
invalidFlag[i] = true;
invalidFlag[j] = true;
}
}
}
List edgeLines = new List();
for (int i = 0; i < triangles.Length; i++)
{
if (!invalidFlag[i])
{
edgeLines.Add(new Vector2Int(edges[i, 0], edges[i, 1]));
}
}
if (edgeLines.Count == 0)
{
Debug.Log("Calculate wrong, there is not any valid line");
}
return edgeLines;
}
///
/// 边缘排序与分离
///
///
///
///
private List> SpliteLines(List edgeLines, Vector3[] vertices)
{
List> result = new List>();
List edgeIndex = new List();
int startIndex = edgeLines[0].x;
edgeIndex.Add(edgeLines[0].x);
int removeIndex = 0;
int currentIndex = edgeLines[0].y;
while (true)
{
edgeLines.RemoveAt(removeIndex);
edgeIndex.Add(currentIndex);
bool findNew = false;
for (int i = 0; i < edgeLines.Count && !findNew; i++)
{
if (currentIndex == edgeLines[i].x)
{
currentIndex = edgeLines[i].y;
removeIndex = i;
findNew = true;
}
else if (currentIndex == edgeLines[i].y)
{
currentIndex = edgeLines[i].x;
removeIndex = i;
findNew = true;
}
}
if (findNew && currentIndex == startIndex)
{
Debug.Log("Complete Closed curve");
edgeLines.RemoveAt(removeIndex);
List singleVertices = new List();
for (int i = 0; i < edgeIndex.Count; i++)
singleVertices.Add(vertices[edgeIndex[i]]);
result.Add(singleVertices);
if (edgeLines.Count > 0)
{
edgeIndex = new List();
startIndex = edgeLines[0].x;
edgeIndex.Add(edgeLines[0].x);
removeIndex = 0;
currentIndex = edgeLines[0].y;
}
else
{
break;
}
}
else if (!findNew)
{
Debug.Log("Complete curve, but not closed");
List singleVertices = new List();
for (int i = 0; i < edgeIndex.Count; i++)
singleVertices.Add(vertices[edgeIndex[i]]);
result.Add(singleVertices);
if (edgeLines.Count > 0)
{
edgeIndex = new List();
startIndex = edgeLines[0].x;
edgeIndex.Add(edgeLines[0].x);
removeIndex = 0;
currentIndex = edgeLines[0].y;
}
else
{
break;
}
}
}
return result;
}
}
LineRenderer组件绘制线,会沿着模型点绘制,实际上我们需要线绘制到模型的内侧(或者外侧)。
在Unity中,从一个方向观察Mesh的三角面顶点排序为逆时针时,这个方向看到的就是三角面的正面。
即俯视三角面的Front时,三角面顶点排序为逆时针;我们最后得到的线为L,沿着L的方向看,图形始终在左手边。
这些调整可以直接在材质中处理: