Unity动态绘制曲线Mesh的代码

来源:互联网 发布:如何做网络推广员 编辑:程序博客网 时间:2024/04/21 00:01

今天给大家介绍一个自己写的小工具,曲线Mesh生成器。起初是为了在地图界面绘制可修改的路径曲线,节约美术人员工作量而开发的小东西。

这是游戏中效果:

这是编辑器窗口里的样子:


这是Inspector里的样子:


这个小工具就一个脚本:CurveMeshBuilder,它在场景里创建一个曲线形状的3D模型,配上贴图就能显示出曲线效果。在编辑器里我们可以看到它提供了几个配置参数,包括曲线的宽度、细腻度和贴图的重复密度,还有路径关键点的调整。

说说这个工具的基本工作流程(简要说下做法,细节实现就不多说了,自己看代码):

1. 生成曲线。用多个关键点构造出一段曲线的方法很多,耳熟能详的贝塞尔曲线就是最出名的一种(它不穿过中途点,所以不适合这里使用)。本文用的是Catmul-Rom曲线,一个常见的曲线生成计算方法,有很多文章介绍,此处就不再介绍了。

2. 计算模型边线。我们算出曲线后,根据精度需要截取一定数目的中间点,串联起来就得到了一组线段,它可以近似描述我们的曲线(我们所要做的就是在性能和效果之间做取舍)。然后将每段线段(或者说向量)向两侧平移,再重新计算接缝,如此获得了两组新的线段,所有线段的端点集合在一起,就是模型的顶点集合。

3. 计算顶点、三角形、UV信息。使顶点们两上两下组合为一个个四边形,再将每个四边形拆分为2个三角形,并计算出每个顶点的UV坐标。这部分内容本来想单独开一篇说明,可是网上的相关文章已经够多够详细了,感觉意义不是很大(一定不是因为懒)。

4. 填充Mesh。将顶点信息、三角形信息和UV信息都填充到新建的Mesh里,新鲜出炉的Mesh数据就制成了。

说点补充事项:

1. 为了让贴图循环展现,使得UV参数会大于1。所以图片文件的WrapMode必须是Repeat。

2. 如果有个二维向量的值是(a, b),那么它的垂直向量是(-b, a)和(b, -a),一个在它左侧一个在它右侧。自己画个坐标系看一眼就明白了。

最后上代码。这是Component代码:

using UnityEngine;using System.Collections;using System.Collections.Generic;/// <summary>/// Dynamic build curve mesh by given key points/// Curve type is Catmul-Rom/// </summary>[ExecuteInEditMode][RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]public class CurveMeshBuilder : MonoBehaviour{public struct CurveSegment2D{public Vector2 point1;public Vector2 point2;public CurveSegment2D(Vector2 point1, Vector2 point2){this.point1 = point1;this.point2 = point2;}public Vector2 SegmentVector{get{return point2 - point1;}}}[HideInInspector]public List<Vector2> nodeList = new List<Vector2>();public bool drawGizmos = true;public int smooth = 5;public float width = 0.2f;public float uvTiling = 1f;private Mesh _mesh;#if UNITY_EDITORpublic float gizmosNodeBallSize = 0.1f;[System.NonSerialized]public int selectedNodeIndex = -1;#endifvoid Awake(){Init();BuildMesh();}void Start(){}void Update(){}void Init(){if (_mesh == null){_mesh = new Mesh();_mesh.name = "CurveMesh";GetComponent<MeshFilter>().mesh = _mesh;}}#if UNITY_EDITOR//Draw the spline in the scene viewvoid OnDrawGizmos(){if (!drawGizmos){return;}Vector3 prevPosition = Vector3.zero;for (int i = 0; i < nodeList.Count; i++){if (i == 0){prevPosition = transform.TransformPoint(nodeList[i]);}else{Vector3 curPosition = transform.TransformPoint(nodeList[i]);Gizmos.DrawLine(prevPosition, curPosition);prevPosition = curPosition;}if (i == selectedNodeIndex){Color c = Gizmos.color;Gizmos.color = Color.yellow;Gizmos.DrawSphere(prevPosition, gizmosNodeBallSize * UnityEditor.HandleUtility.GetHandleSize(prevPosition) * 1.5f);Gizmos.color = c;}else{Gizmos.DrawSphere(prevPosition, gizmosNodeBallSize * UnityEditor.HandleUtility.GetHandleSize(prevPosition));}}}#endif#region Node Operatepublic void AddNode(Vector2 position){nodeList.Add(position);}public void InsertNode(int index, Vector2 position){index = Mathf.Max(index, 0);if (index >= nodeList.Count){AddNode(position);}else{nodeList.Insert(index, position);}}public void RemoveNode(int index){if (index < 0 || index >= nodeList.Count){return;}nodeList.RemoveAt(index);}public void ClearNodes(){nodeList.Clear();}#endregionpublic bool BuildMesh(){Init();_mesh.Clear();if (nodeList.Count < 2){return false;}List<Vector2> curvePoints = CalculateCurve(nodeList, smooth, false);List<Vector2> vertices = GetVertices(curvePoints, width * 0.5f);List<Vector2> verticesUV = GetVerticesUV(curvePoints);Vector3[] _vertices = new Vector3[vertices.Count];Vector2[] _uv = new Vector2[verticesUV.Count];int[] _triangles = new int[(vertices.Count - 2) * 3];for (int i = 0; i < vertices.Count; i++){_vertices[i].Set(vertices[i].x, vertices[i].y, 0);}for (int i = 0; i < verticesUV.Count; i++){_uv[i].Set(verticesUV[i].x, verticesUV[i].y);}for (int i = 2; i < vertices.Count; i += 2){int index = (i - 2) * 3;_triangles[index] = i - 2;_triangles[index + 1] = i - 0;_triangles[index + 2] = i - 1;_triangles[index + 3] = i - 1;_triangles[index + 4] = i - 0;_triangles[index + 5] = i + 1;}_mesh.vertices = _vertices;_mesh.triangles = _triangles;_mesh.uv = _uv;_mesh.RecalculateNormals();return true;}/// <summary>/// Calculate Catmul-Rom Curve/// </summary>/// <param name="points">key points</param>/// <param name="smooth">how many segments between two nearby point</param>/// <param name="curveClose">whether curve is a circle</param>/// <returns></returns>public List<Vector2> CalculateCurve(IList<Vector2> points, int smooth, bool curveClose){int pointCount = points.Count;int segmentCount = curveClose ? pointCount : pointCount - 1;List<Vector2> allVertices = new List<Vector2>((smooth + 1) * segmentCount);Vector2[] tempVertices = new Vector2[smooth + 1];float smoothReciprocal = 1f / smooth;for (int i = 0; i < segmentCount; ++i){// get 4 adjacent point in points to calculate position between p1 and p2Vector2 p0, p1, p2, p3;p1 = points[i];if (curveClose){p0 = i == 0 ? points[segmentCount - 1] : points[i - 1];p2 = i + 1 < pointCount ? points[i + 1] : points[i + 1 - pointCount];p3 = i + 2 < pointCount ? points[i + 2] : points[i + 2 - pointCount];}else{p0 = i == 0 ? p1 : points[i - 1];p2 = points[i + 1];p3 = i == segmentCount - 1 ? p2 : points[i + 2];}Vector2 pA = p1;Vector2 pB = 0.5f * (-p0 + p2);Vector2 pC = p0 - 2.5f * p1 + 2f * p2 - 0.5f * p3;Vector2 pD = 0.5f * (-p0 + 3f * p1 - 3f * p2 + p3);float t = 0;for (int j = 0; j <= smooth; j++){tempVertices[j] = pA + t * (pB + t * (pC + t * pD));t += smoothReciprocal;}for (int j = allVertices.Count == 0 ? 0 : 1; j < tempVertices.Length; j++){allVertices.Add(tempVertices[j]);}}return allVertices;}private List<CurveSegment2D> GetSegments(List<Vector2> points){List<CurveSegment2D> segments = new List<CurveSegment2D>(points.Count - 1);for (int i = 1; i < points.Count; i++){segments.Add(new CurveSegment2D(points[i - 1], points[i]));}return segments;}private List<Vector2> GetVertices(List<Vector2> points, float expands){List<CurveSegment2D> segments = GetSegments(points);List<CurveSegment2D> segments1 = new List<CurveSegment2D>(segments.Count);List<CurveSegment2D> segments2 = new List<CurveSegment2D>(segments.Count);for (int i = 0; i < segments.Count; i++){Vector2 vOffset = new Vector2(-segments[i].SegmentVector.y, segments[i].SegmentVector.x).normalized;segments1.Add(new CurveSegment2D(segments[i].point1 + vOffset * expands, segments[i].point2 + vOffset * expands));segments2.Add(new CurveSegment2D(segments[i].point1 - vOffset * expands, segments[i].point2 - vOffset * expands));}List<Vector2> points1 = new List<Vector2>(points.Count);List<Vector2> points2 = new List<Vector2>(points.Count);for (int i = 0; i < segments1.Count; i++){if (i == 0){points1.Add(segments1[0].point1);}else{Vector2 crossPoint;if (!TryCalculateLinesIntersection(segments1[i - 1], segments1[i], out crossPoint, 0.1f)){crossPoint = segments1[i].point1;}points1.Add(crossPoint);}if (i == segments1.Count - 1){points1.Add(segments1[i].point2);}}for (int i = 0; i < segments2.Count; i++){if (i == 0){points2.Add(segments2[0].point1);}else{Vector2 crossPoint;if (!TryCalculateLinesIntersection(segments2[i - 1], segments2[i], out crossPoint, 0.1f)){crossPoint = segments2[i].point1;}points2.Add(crossPoint);}if (i == segments2.Count - 1){points2.Add(segments2[i].point2);}}List<Vector2> combinePoints = new List<Vector2>(points.Count * 2);for (int i = 0; i < points.Count; i++){combinePoints.Add(points1[i]);combinePoints.Add(points2[i]);}return combinePoints;}private List<Vector2> GetVerticesUV(List<Vector2> points){List<Vector2> uvs = new List<Vector2>(points.Count * 2);float totalLength = 0;float totalLengthReciprocal = 0;float curLength = 0;for (int i = 1; i < points.Count; i++){totalLength += Vector2.Distance(points[i - 1], points[i]);}totalLengthReciprocal = uvTiling / totalLength;for (int i = 0; i < points.Count; i++){if (i == 0){uvs.Add(new Vector2(0, 1));uvs.Add(new Vector2(0, 0));}else{if (i == points.Count - 1){uvs.Add(new Vector2(uvTiling, 1));uvs.Add(new Vector2(uvTiling, 0));}else{curLength += Vector2.Distance(points[i - 1], points[i]);float uvx = curLength * totalLengthReciprocal;uvs.Add(new Vector2(uvx, 1));uvs.Add(new Vector2(uvx, 0));}}}return uvs;}private bool TryCalculateLinesIntersection(CurveSegment2D segment1, CurveSegment2D segment2, out Vector2 intersection, float angleLimit){intersection = new Vector2();Vector2 p1 = segment1.point1;Vector2 p2 = segment1.point2;Vector2 p3 = segment2.point1;Vector2 p4 = segment2.point2;float denominator = (p2.y - p1.y) * (p4.x - p3.x) - (p1.x - p2.x) * (p3.y - p4.y);// If denominator is 0, means parallelif (denominator == 0){return false;}// Check angle between segmentsfloat angle = Vector2.Angle(segment1.SegmentVector, segment2.SegmentVector);// if the angle between two segments is too small, we treat them as parallelif (angle < angleLimit || (180f - angle) < angleLimit){return false;}float x = ((p2.x - p1.x) * (p4.x - p3.x) * (p3.y - p1.y)+ (p2.y - p1.y) * (p4.x - p3.x) * p1.x- (p4.y - p3.y) * (p2.x - p1.x) * p3.x) / denominator;float y = -((p2.y - p1.y) * (p4.y - p3.y) * (p3.x - p1.x)+ (p2.x - p1.x) * (p4.y - p3.y) * p1.y- (p4.x - p3.x) * (p2.y - p1.y) * p3.y) / denominator;intersection.Set(x, y);return true;}}
这是Editor代码:

using UnityEngine;using UnityEditor;using System.Collections;using System.Collections.Generic;[CustomEditor(typeof(CurveMeshBuilder))]public class CurveMeshBuilderEditor : Editor{private CurveMeshBuilder _script;private GUIStyle _guiStyle_Border1;private GUIStyle _guiStyle_Border2;private GUIStyle _guiStyle_Border3;private GUIStyle _guiStyle_Button1;private GUIStyle _guiStyle_Button2;void Awake(){_script = target as CurveMeshBuilder;_guiStyle_Border1 = new GUIStyle("sv_iconselector_back");_guiStyle_Border1.stretchHeight = false;_guiStyle_Border1.padding = new RectOffset(4, 4, 4, 4);_guiStyle_Border2 = new GUIStyle("U2D.createRect");_guiStyle_Border3 = new GUIStyle("SelectionRect");_guiStyle_Border3.padding = new RectOffset(6, 6, 6, 6);_guiStyle_Button1 = new GUIStyle("PreButton");_guiStyle_Button2 = new GUIStyle("horizontalsliderthumb");}public override void OnInspectorGUI(){base.OnInspectorGUI();EditorGUILayout.BeginVertical(_guiStyle_Border1);{if (_script.nodeList.Count < 2){GUILayout.Label("Key points num should not less than 2 !", "CN EntryWarn");}for (int i = 0; i < _script.nodeList.Count; i++){EditorGUILayout.BeginHorizontal(i == _script.selectedNodeIndex ? _guiStyle_Border2 : _guiStyle_Border3);{if (GUILayout.Button("", _guiStyle_Button2, GUILayout.Width(20))){_script.selectedNodeIndex = i;}GUILayout.Space(2);GUILayout.Label((i + 1).ToString());Vector2 newNodePos = EditorGUILayout.Vector2Field("", _script.nodeList[i]);if (_script.nodeList[i] != newNodePos){_script.nodeList[i] = newNodePos;}GUILayout.Space(6);if (GUILayout.Button("<", _guiStyle_Button1, GUILayout.Width(20))){Vector2 pos = i == 0 ? _script.nodeList[i] - Vector2.right : (_script.nodeList[i - 1] + _script.nodeList[i]) * 0.5f;_script.InsertNode(i, pos);_script.selectedNodeIndex = i;}GUILayout.Space(2);if (GUILayout.Button("✖", _guiStyle_Button1, GUILayout.Width(20))){_script.RemoveNode(i);_script.selectedNodeIndex = i < _script.nodeList.Count ? i : i - 1;}}EditorGUILayout.EndHorizontal();}EditorGUILayout.BeginHorizontal();{if (GUILayout.Button("Add", _guiStyle_Button1)){Vector2 pos = _script.nodeList.Count == 0 ? Vector2.zero : _script.nodeList[_script.nodeList.Count - 1] + Vector2.right;_script.AddNode(pos);_script.selectedNodeIndex = _script.nodeList.Count - 1;}if (GUILayout.Button("Clear", _guiStyle_Button1)){_script.ClearNodes();}}EditorGUILayout.EndHorizontal();}EditorGUILayout.EndVertical();if (GUILayout.Button("Build Model")){_script.BuildMesh();}if (GUI.changed){EditorUtility.SetDirty(target);}}}
最后感谢:插件Math Library For Unity。本文参考了该插件的曲线工具,也推荐大家使用这个实用性极强的数学扩展插件,其丰富的功能尤其是常见几何图形的边界计算,能大幅提高生产效率,是每个开发者的必备插件。
0 0
原创粉丝点击