赞
踩
今天给大家介绍一个自己写的小工具,曲线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代码:
- 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_EDITOR
- public float gizmosNodeBallSize = 0.1f;
- [System.NonSerialized]
- public int selectedNodeIndex = -1;
- #endif
-
- void 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 view
- void 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 Operate
- public 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();
- }
- #endregion
-
- public 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 p2
- Vector2 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 parallel
- if (denominator == 0)
- {
- return false;
- }
-
- // Check angle between segments
- float angle = Vector2.Angle(segment1.SegmentVector, segment2.SegmentVector);
- // if the angle between two segments is too small, we treat them as parallel
- if (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。本文参考了该插件的曲线工具,也推荐大家使用这个实用性极强的数学扩展插件,其丰富的功能尤其是常见几何图形的边界计算,能大幅提高生产效率,是每个开发者的必备插件。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。