当前位置:   article > 正文

Unity动态绘制曲线Mesh的代码_unity画线生成面

unity画线生成面

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

这是游戏中效果:

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


这是Inspector里的样子:


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

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

1. 计算曲线。用多个关键点构造出一段曲线的方法很多,耳熟能详的贝塞尔曲线就是最出名的一种(它不穿过中途点,所以不适合这里使用)。本文用的是Catmul-Rom曲线,一个常见的曲线生成计算方法,这里不赘述其原理,有兴趣的同学请百度查询。总之,一个合适的曲线算法,在输入一组关键点后,就能生成对应的一条曲线数据。

2. 得到近似表达曲线的线段组。有了曲线后,根据精度需要截取一定数目的中间点,串联起来就得到了一组首尾相连的线段,它可以近似描述我们的曲线(中间点越密集,线段就越多,效果就越精细,当然性能也差了,我们所要做的就是在性能和效果之间做取舍)。

3. 一组没有宽度的线段是没法组成模型的,我们需要把这组线段扩展为平行的两组线段,变为一组四边形的集合。如下图所示,线段A-B、B-C向上平移可得到a-b1、b2-c两个线段,再算出其交点b,得到a-b、b-c两个线段(或者说abc三个点,对应于ABC点)。再用同样的手段得到向下平移的3个点,我们就得到了由2个四边形构成的有宽度的2个线段。


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

5. 用代码创建Mesh。将上面算得的顶点信息、三角形信息和UV信息都填充到Mesh里,新鲜出炉的动态Mesh就制成了。

说点补充事项:

1. 为了让贴图循环展现,图片文件的WrapMode必须是Repeat。

2. 在上面的图中,为了得到a点,我们需要先算出AB的垂直向量Aa,这个向量其实很好算,假定向量AB的数值是(x,y),那么向量(-y,x)就与Aa同向,将(-y,x)的长度做下调整,就能得到向量Aa的值。

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

  1. using UnityEngine;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. /// <summary>
  5. /// Dynamic build curve mesh by given key points
  6. /// Curve type is Catmul-Rom
  7. /// </summary>
  8. [ExecuteInEditMode]
  9. [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
  10. public class CurveMeshBuilder : MonoBehaviour
  11. {
  12. public struct CurveSegment2D
  13. {
  14. public Vector2 point1;
  15. public Vector2 point2;
  16. public CurveSegment2D(Vector2 point1, Vector2 point2)
  17. {
  18. this.point1 = point1;
  19. this.point2 = point2;
  20. }
  21. public Vector2 SegmentVector
  22. {
  23. get
  24. {
  25. return point2 - point1;
  26. }
  27. }
  28. }
  29. [HideInInspector]
  30. public List<Vector2> nodeList = new List<Vector2>();
  31. public bool drawGizmos = true;
  32. public int smooth = 5;
  33. public float width = 0.2f;
  34. public float uvTiling = 1f;
  35. private Mesh _mesh;
  36. #if UNITY_EDITOR
  37. public float gizmosNodeBallSize = 0.1f;
  38. [System.NonSerialized]
  39. public int selectedNodeIndex = -1;
  40. #endif
  41. void Awake()
  42. {
  43. Init();
  44. BuildMesh();
  45. }
  46. void Start()
  47. {
  48. }
  49. void Update()
  50. {
  51. }
  52. void Init()
  53. {
  54. if (_mesh == null)
  55. {
  56. _mesh = new Mesh();
  57. _mesh.name = "CurveMesh";
  58. GetComponent<MeshFilter>().mesh = _mesh;
  59. }
  60. }
  61. #if UNITY_EDITOR
  62. //Draw the spline in the scene view
  63. void OnDrawGizmos()
  64. {
  65. if (!drawGizmos)
  66. {
  67. return;
  68. }
  69. Vector3 prevPosition = Vector3.zero;
  70. for (int i = 0; i < nodeList.Count; i++)
  71. {
  72. if (i == 0)
  73. {
  74. prevPosition = transform.TransformPoint(nodeList[i]);
  75. }
  76. else
  77. {
  78. Vector3 curPosition = transform.TransformPoint(nodeList[i]);
  79. Gizmos.DrawLine(prevPosition, curPosition);
  80. prevPosition = curPosition;
  81. }
  82. if (i == selectedNodeIndex)
  83. {
  84. Color c = Gizmos.color;
  85. Gizmos.color = Color.yellow;
  86. Gizmos.DrawSphere(prevPosition, gizmosNodeBallSize * UnityEditor.HandleUtility.GetHandleSize(prevPosition) * 1.5f);
  87. Gizmos.color = c;
  88. }
  89. else
  90. {
  91. Gizmos.DrawSphere(prevPosition, gizmosNodeBallSize * UnityEditor.HandleUtility.GetHandleSize(prevPosition));
  92. }
  93. }
  94. }
  95. #endif
  96. #region Node Operate
  97. public void AddNode(Vector2 position)
  98. {
  99. nodeList.Add(position);
  100. }
  101. public void InsertNode(int index, Vector2 position)
  102. {
  103. index = Mathf.Max(index, 0);
  104. if (index >= nodeList.Count)
  105. {
  106. AddNode(position);
  107. }
  108. else
  109. {
  110. nodeList.Insert(index, position);
  111. }
  112. }
  113. public void RemoveNode(int index)
  114. {
  115. if (index < 0 || index >= nodeList.Count)
  116. {
  117. return;
  118. }
  119. nodeList.RemoveAt(index);
  120. }
  121. public void ClearNodes()
  122. {
  123. nodeList.Clear();
  124. }
  125. #endregion
  126. public bool BuildMesh()
  127. {
  128. Init();
  129. _mesh.Clear();
  130. if (nodeList.Count < 2)
  131. {
  132. return false;
  133. }
  134. List<Vector2> curvePoints = CalculateCurve(nodeList, smooth, false);
  135. List<Vector2> vertices = GetVertices(curvePoints, width * 0.5f);
  136. List<Vector2> verticesUV = GetVerticesUV(curvePoints);
  137. Vector3[] _vertices = new Vector3[vertices.Count];
  138. Vector2[] _uv = new Vector2[verticesUV.Count];
  139. int[] _triangles = new int[(vertices.Count - 2) * 3];
  140. for (int i = 0; i < vertices.Count; i++)
  141. {
  142. _vertices[i].Set(vertices[i].x, vertices[i].y, 0);
  143. }
  144. for (int i = 0; i < verticesUV.Count; i++)
  145. {
  146. _uv[i].Set(verticesUV[i].x, verticesUV[i].y);
  147. }
  148. for (int i = 2; i < vertices.Count; i += 2)
  149. {
  150. int index = (i - 2) * 3;
  151. _triangles[index] = i - 2;
  152. _triangles[index + 1] = i - 0;
  153. _triangles[index + 2] = i - 1;
  154. _triangles[index + 3] = i - 1;
  155. _triangles[index + 4] = i - 0;
  156. _triangles[index + 5] = i + 1;
  157. }
  158. _mesh.vertices = _vertices;
  159. _mesh.triangles = _triangles;
  160. _mesh.uv = _uv;
  161. _mesh.RecalculateNormals();
  162. return true;
  163. }
  164. /// <summary>
  165. /// Calculate Catmul-Rom Curve
  166. /// </summary>
  167. /// <param name="points">key points</param>
  168. /// <param name="smooth">how many segments between two nearby point</param>
  169. /// <param name="curveClose">whether curve is a circle</param>
  170. /// <returns></returns>
  171. public List<Vector2> CalculateCurve(IList<Vector2> points, int smooth, bool curveClose)
  172. {
  173. int pointCount = points.Count;
  174. int segmentCount = curveClose ? pointCount : pointCount - 1;
  175. List<Vector2> allVertices = new List<Vector2>((smooth + 1) * segmentCount);
  176. Vector2[] tempVertices = new Vector2[smooth + 1];
  177. float smoothReciprocal = 1f / smooth;
  178. for (int i = 0; i < segmentCount; ++i)
  179. {
  180. // get 4 adjacent point in points to calculate position between p1 and p2
  181. Vector2 p0, p1, p2, p3;
  182. p1 = points[i];
  183. if (curveClose)
  184. {
  185. p0 = i == 0 ? points[segmentCount - 1] : points[i - 1];
  186. p2 = i + 1 < pointCount ? points[i + 1] : points[i + 1 - pointCount];
  187. p3 = i + 2 < pointCount ? points[i + 2] : points[i + 2 - pointCount];
  188. }
  189. else
  190. {
  191. p0 = i == 0 ? p1 : points[i - 1];
  192. p2 = points[i + 1];
  193. p3 = i == segmentCount - 1 ? p2 : points[i + 2];
  194. }
  195. Vector2 pA = p1;
  196. Vector2 pB = 0.5f * (-p0 + p2);
  197. Vector2 pC = p0 - 2.5f * p1 + 2f * p2 - 0.5f * p3;
  198. Vector2 pD = 0.5f * (-p0 + 3f * p1 - 3f * p2 + p3);
  199. float t = 0;
  200. for (int j = 0; j <= smooth; j++)
  201. {
  202. tempVertices[j] = pA + t * (pB + t * (pC + t * pD));
  203. t += smoothReciprocal;
  204. }
  205. for (int j = allVertices.Count == 0 ? 0 : 1; j < tempVertices.Length; j++)
  206. {
  207. allVertices.Add(tempVertices[j]);
  208. }
  209. }
  210. return allVertices;
  211. }
  212. private List<CurveSegment2D> GetSegments(List<Vector2> points)
  213. {
  214. List<CurveSegment2D> segments = new List<CurveSegment2D>(points.Count - 1);
  215. for (int i = 1; i < points.Count; i++)
  216. {
  217. segments.Add(new CurveSegment2D(points[i - 1], points[i]));
  218. }
  219. return segments;
  220. }
  221. private List<Vector2> GetVertices(List<Vector2> points, float expands)
  222. {
  223. List<CurveSegment2D> segments = GetSegments(points);
  224. List<CurveSegment2D> segments1 = new List<CurveSegment2D>(segments.Count);
  225. List<CurveSegment2D> segments2 = new List<CurveSegment2D>(segments.Count);
  226. for (int i = 0; i < segments.Count; i++)
  227. {
  228. Vector2 vOffset = new Vector2(-segments[i].SegmentVector.y, segments[i].SegmentVector.x).normalized;
  229. segments1.Add(new CurveSegment2D(segments[i].point1 + vOffset * expands, segments[i].point2 + vOffset * expands));
  230. segments2.Add(new CurveSegment2D(segments[i].point1 - vOffset * expands, segments[i].point2 - vOffset * expands));
  231. }
  232. List<Vector2> points1 = new List<Vector2>(points.Count);
  233. List<Vector2> points2 = new List<Vector2>(points.Count);
  234. for (int i = 0; i < segments1.Count; i++)
  235. {
  236. if (i == 0)
  237. {
  238. points1.Add(segments1[0].point1);
  239. }
  240. else
  241. {
  242. Vector2 crossPoint;
  243. if (!TryCalculateLinesIntersection(segments1[i - 1], segments1[i], out crossPoint, 0.1f))
  244. {
  245. crossPoint = segments1[i].point1;
  246. }
  247. points1.Add(crossPoint);
  248. }
  249. if (i == segments1.Count - 1)
  250. {
  251. points1.Add(segments1[i].point2);
  252. }
  253. }
  254. for (int i = 0; i < segments2.Count; i++)
  255. {
  256. if (i == 0)
  257. {
  258. points2.Add(segments2[0].point1);
  259. }
  260. else
  261. {
  262. Vector2 crossPoint;
  263. if (!TryCalculateLinesIntersection(segments2[i - 1], segments2[i], out crossPoint, 0.1f))
  264. {
  265. crossPoint = segments2[i].point1;
  266. }
  267. points2.Add(crossPoint);
  268. }
  269. if (i == segments2.Count - 1)
  270. {
  271. points2.Add(segments2[i].point2);
  272. }
  273. }
  274. List<Vector2> combinePoints = new List<Vector2>(points.Count * 2);
  275. for (int i = 0; i < points.Count; i++)
  276. {
  277. combinePoints.Add(points1[i]);
  278. combinePoints.Add(points2[i]);
  279. }
  280. return combinePoints;
  281. }
  282. private List<Vector2> GetVerticesUV(List<Vector2> points)
  283. {
  284. List<Vector2> uvs = new List<Vector2>(points.Count * 2);
  285. float totalLength = 0;
  286. float totalLengthReciprocal = 0;
  287. float curLength = 0;
  288. for (int i = 1; i < points.Count; i++)
  289. {
  290. totalLength += Vector2.Distance(points[i - 1], points[i]);
  291. }
  292. totalLengthReciprocal = uvTiling / totalLength;
  293. for (int i = 0; i < points.Count; i++)
  294. {
  295. if (i == 0)
  296. {
  297. uvs.Add(new Vector2(0, 1));
  298. uvs.Add(new Vector2(0, 0));
  299. }
  300. else
  301. {
  302. if (i == points.Count - 1)
  303. {
  304. uvs.Add(new Vector2(uvTiling, 1));
  305. uvs.Add(new Vector2(uvTiling, 0));
  306. }
  307. else
  308. {
  309. curLength += Vector2.Distance(points[i - 1], points[i]);
  310. float uvx = curLength * totalLengthReciprocal;
  311. uvs.Add(new Vector2(uvx, 1));
  312. uvs.Add(new Vector2(uvx, 0));
  313. }
  314. }
  315. }
  316. return uvs;
  317. }
  318. private bool TryCalculateLinesIntersection(CurveSegment2D segment1, CurveSegment2D segment2, out Vector2 intersection, float angleLimit)
  319. {
  320. intersection = new Vector2();
  321. Vector2 p1 = segment1.point1;
  322. Vector2 p2 = segment1.point2;
  323. Vector2 p3 = segment2.point1;
  324. Vector2 p4 = segment2.point2;
  325. float denominator = (p2.y - p1.y) * (p4.x - p3.x) - (p1.x - p2.x) * (p3.y - p4.y);
  326. // If denominator is 0, means parallel
  327. if (denominator == 0)
  328. {
  329. return false;
  330. }
  331. // Check angle between segments
  332. float angle = Vector2.Angle(segment1.SegmentVector, segment2.SegmentVector);
  333. // if the angle between two segments is too small, we treat them as parallel
  334. if (angle < angleLimit || (180f - angle) < angleLimit)
  335. {
  336. return false;
  337. }
  338. float x = ((p2.x - p1.x) * (p4.x - p3.x) * (p3.y - p1.y)
  339. + (p2.y - p1.y) * (p4.x - p3.x) * p1.x
  340. - (p4.y - p3.y) * (p2.x - p1.x) * p3.x) / denominator;
  341. float y = -((p2.y - p1.y) * (p4.y - p3.y) * (p3.x - p1.x)
  342. + (p2.x - p1.x) * (p4.y - p3.y) * p1.y
  343. - (p4.x - p3.x) * (p2.y - p1.y) * p3.y) / denominator;
  344. intersection.Set(x, y);
  345. return true;
  346. }
  347. }
这是Editor代码:

  1. using UnityEngine;
  2. using UnityEditor;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. [CustomEditor(typeof(CurveMeshBuilder))]
  6. public class CurveMeshBuilderEditor : Editor
  7. {
  8. private CurveMeshBuilder _script;
  9. private GUIStyle _guiStyle_Border1;
  10. private GUIStyle _guiStyle_Border2;
  11. private GUIStyle _guiStyle_Border3;
  12. private GUIStyle _guiStyle_Button1;
  13. private GUIStyle _guiStyle_Button2;
  14. void Awake()
  15. {
  16. _script = target as CurveMeshBuilder;
  17. _guiStyle_Border1 = new GUIStyle("sv_iconselector_back");
  18. _guiStyle_Border1.stretchHeight = false;
  19. _guiStyle_Border1.padding = new RectOffset(4, 4, 4, 4);
  20. _guiStyle_Border2 = new GUIStyle("U2D.createRect");
  21. _guiStyle_Border3 = new GUIStyle("SelectionRect");
  22. _guiStyle_Border3.padding = new RectOffset(6, 6, 6, 6);
  23. _guiStyle_Button1 = new GUIStyle("PreButton");
  24. _guiStyle_Button2 = new GUIStyle("horizontalsliderthumb");
  25. }
  26. public override void OnInspectorGUI()
  27. {
  28. base.OnInspectorGUI();
  29. EditorGUILayout.BeginVertical(_guiStyle_Border1);
  30. {
  31. if (_script.nodeList.Count < 2)
  32. {
  33. GUILayout.Label("Key points num should not less than 2 !", "CN EntryWarn");
  34. }
  35. for (int i = 0; i < _script.nodeList.Count; i++)
  36. {
  37. EditorGUILayout.BeginHorizontal(i == _script.selectedNodeIndex ? _guiStyle_Border2 : _guiStyle_Border3);
  38. {
  39. if (GUILayout.Button("", _guiStyle_Button2, GUILayout.Width(20)))
  40. {
  41. _script.selectedNodeIndex = i;
  42. }
  43. GUILayout.Space(2);
  44. GUILayout.Label((i + 1).ToString());
  45. Vector2 newNodePos = EditorGUILayout.Vector2Field("", _script.nodeList[i]);
  46. if (_script.nodeList[i] != newNodePos)
  47. {
  48. _script.nodeList[i] = newNodePos;
  49. }
  50. GUILayout.Space(6);
  51. if (GUILayout.Button("<", _guiStyle_Button1, GUILayout.Width(20)))
  52. {
  53. Vector2 pos = i == 0 ? _script.nodeList[i] - Vector2.right : (_script.nodeList[i - 1] + _script.nodeList[i]) * 0.5f;
  54. _script.InsertNode(i, pos);
  55. _script.selectedNodeIndex = i;
  56. }
  57. GUILayout.Space(2);
  58. if (GUILayout.Button("✖", _guiStyle_Button1, GUILayout.Width(20)))
  59. {
  60. _script.RemoveNode(i);
  61. _script.selectedNodeIndex = i < _script.nodeList.Count ? i : i - 1;
  62. }
  63. }
  64. EditorGUILayout.EndHorizontal();
  65. }
  66. EditorGUILayout.BeginHorizontal();
  67. {
  68. if (GUILayout.Button("Add", _guiStyle_Button1))
  69. {
  70. Vector2 pos = _script.nodeList.Count == 0 ? Vector2.zero : _script.nodeList[_script.nodeList.Count - 1] + Vector2.right;
  71. _script.AddNode(pos);
  72. _script.selectedNodeIndex = _script.nodeList.Count - 1;
  73. }
  74. if (GUILayout.Button("Clear", _guiStyle_Button1))
  75. {
  76. _script.ClearNodes();
  77. }
  78. }
  79. EditorGUILayout.EndHorizontal();
  80. }
  81. EditorGUILayout.EndVertical();
  82. if (GUILayout.Button("Build Model"))
  83. {
  84. _script.BuildMesh();
  85. }
  86. if (GUI.changed)
  87. {
  88. EditorUtility.SetDirty(target);
  89. }
  90. }
  91. }
最后感谢:插件Math Library For Unity。本文参考了该插件的曲线工具,也推荐大家使用这个实用性极强的数学扩展插件,其丰富的功能尤其是常见几何图形的边界计算,能大幅提高生产效率,是每个开发者的必备插件。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/580279
推荐阅读
相关标签
  

闽ICP备14008679号