Unity中的曲线绘制(三)——样条曲线
发表于2016-11-02
单独的一条贝塞尔曲线很容易生成,但是它并不能模拟更复杂的路径,但是我们可以通过组合不同的贝塞尔曲线,来得到复杂的曲线。复制BezierCurve的代码,修改为BezierSpline类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | using UnityEngine; public class BezierSpline : MonoBehaviour { public Vector3[] points; public Vector3 GetPoint ( float t) { return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t)); } public Vector3 GetVelocity ( float t) { return transform.TransformPoint( Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position; } public Vector3 GetDirection ( float t) { return GetVelocity(t).normalized; } public void Reset () { points = new Vector3[] { new Vector3(1f, 0f, 0f), new Vector3(2f, 0f, 0f), new Vector3(3f, 0f, 0f), new Vector3(4f, 0f, 0f) }; } } |
同样为它写一个editor,复制BezierCurveInspector中的代码,进行相应调整。然后我们就可以创建一个样条曲线对象并且在scene视图中编辑它了。
using UnityEditor;
using UnityEngine;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | [CustomEditor( typeof (BezierSpline))] public class BezierSplineInspector : Editor { private const int lineSteps = 10; private const float directionScale = 0.5f; private BezierSpline spline; private Transform handleTransform; private Quaternion handleRotation; private void OnSceneGUI () { spline = target as BezierSpline; handleTransform = spline.transform; handleRotation = Tools.pivotRotation == PivotRotation.Local ? handleTransform.rotation : Quaternion.identity; Vector3 p0 = ShowPoint(0); Vector3 p1 = ShowPoint(1); Vector3 p2 = ShowPoint(2); Vector3 p3 = ShowPoint(3); Handles.color = Color.gray; Handles.DrawLine(p0, p1); Handles.DrawLine(p2, p3); ShowDirections(); Handles.DrawBezier(p0, p3, p1, p2, Color.white, null , 2f); } private void ShowDirections () { Handles.color = Color.green; Vector3 point = spline.GetPoint(0f); Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale); for ( int i = 1; i <= lineSteps; i++) { point = spline.GetPoint(i / ( float )lineSteps); Handles.DrawLine(point, point + spline.GetDirection(i / ( float )lineSteps) * directionScale); } } private Vector3 ShowPoint ( int index) { Vector3 point = handleTransform.TransformPoint(spline.points[index]); EditorGUI.BeginChangeCheck(); point = Handles.DoPositionHandle(point, handleRotation); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(spline, "Move Point" ); EditorUtility.SetDirty(spline); spline.points[index] = handleTransform.InverseTransformPoint(point); } return point; } } |
由于只是简单地复制了一下代码,现在的spline和之前的曲线并无不同。现在来为BezierSpline添加一个方法,用于为spline添加另外的曲线。由于spline必须是连续的,所以第一条曲线的终点就是第二条曲线的起点。要额外添加三个点。
1 2 3 4 5 6 7 8 9 10 11 | public void AddCurve() { Vector3 point = points[points.Length - 1]; // 最后一个点 Array.Resize( ref points, points.Length + 3); // 扩充数组 point.x += 1f; points[points.Length - 3] = point; point.x += 1f; points[points.Length - 2] = point; point.x += 1f; points[points.Length - 1] = point; } |
1 2 | using UnityEngine; using System; |
1 2 3 4 5 6 7 8 9 | public override void OnInspectorGUI () { DrawDefaultInspector(); spline = target as BezierSpline; if (GUILayout.Button( "Add Curve" )) { Undo.RecordObject(spline, "Add Curve" ); spline.AddCurve(); EditorUtility.SetDirty(spline); } } |
但是场景中还是只能看见一条曲线,调整BezierSplineInspector代码来显示所有曲线。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | private void OnSceneGUI () { spline = target as BezierSpline; handleTransform = spline.transform; handleRotation = Tools.pivotRotation == PivotRotation.Local ? handleTransform.rotation : Quaternion.identity; Vector3 p0 = ShowPoint(0); for ( int i = 1; i < spline.points.Length; i += 3) { Vector3 p1 = ShowPoint(i); Vector3 p2 = ShowPoint(i + 1); Vector3 p3 = ShowPoint(i + 2); Handles.color = Color.gray; Handles.DrawLine(p0, p1); Handles.DrawLine(p2, p3); Handles.DrawBezier(p0, p3, p1, p2, Color.white, null , 2f); p0 = p3; } ShowDirections(); } |
现在可以看到所有的曲线都被绘制出来了,但是只有第一条曲线上有速度的标识。这是因为BezierSpline中的方法仍然只对四个点进行处理,所以要对它进行更改。
虽然有多段曲线,但是我们希望只使用一个[0,1]区间内的t来完成整个过程。思路是将t乘以曲线的段数,得到的结果取整数部分,就是曲线的索引。例如有三段曲线, t =0.6,那么乘以3之后得到1.8,整数部分为1,说明当前是在第二段曲线上。因为我们需要知道当前有几段曲线,所以添加一个CurveCount属性。
1 2 3 4 5 | public int CurveCount { get { return (points.Length - 1) / 3; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public Vector3 GetPoint ( float t) { int i; if (t >= 1f) { t = 1f; i = points.Length - 4; } else { t = Mathf.Clamp01(t) * CurveCount; i = ( int )t; t -= i; i *= 3; } return transform.TransformPoint(Bezier.GetPoint( points[i], points[i + 1], points[i + 2], points[i + 3], t)); } public Vector3 GetVelocity ( float t) { int i; if (t >= 1f) { t = 1f; i = points.Length - 4; } else { t = Mathf.Clamp01(t) * CurveCount; i = ( int )t; t -= i; i *= 3; } return transform.TransformPoint(Bezier.GetFirstDerivative( points[i], points[i + 1], points[i + 2], points[i + 3], t)) - transform.position; } |
1 2 3 4 5 6 7 8 9 10 11 12 | private const int stepsPerCurve = 10; private void ShowDirections () { Handles.color = Color.green; Vector3 point = spline.GetPoint(0f); Handles.DrawLine(point, point + spline.GetDirection(0f) * directionScale); int steps = stepsPerCurve * spline.CurveCount; for ( int i = 1; i <= steps; i++) { point = spline.GetPoint(i / ( float )steps); Handles.DrawLine(point, point + spline.GetDirection(i / ( float )steps) * directionScale); } } |
为所有的点显示坐标轴,画面会变得很拥挤,我们可以只显示被选中的点的坐标轴,其他点用小按钮表示。更新ShowPoint方法,让它将控制点显示为小小的按钮,点击某个点时它的坐标轴将会显示。用一个参数selectedIndex代表当前选中按钮的index,默认为-1,代表初始状态没有点被选中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private const float handleSize = 0.04f; private const float pickSize = 0.06f; private int selectedIndex = -1; private Vector3 ShowPoint ( int index) { Vector3 point = handleTransform.TransformPoint(spline.points[index]); Handles.color = Color.white; if (Handles.Button(point, handleRotation, handleSize, pickSize, Handles.DotCap)) { selectedIndex = index; } if (selectedIndex == index) { EditorGUI.BeginChangeCheck(); point = Handles.DoPositionHandle(point, handleRotation); if (EditorGUI.EndChangeCheck()) { Undo.RecordObject(spline, "Move Point" ); EditorUtility.SetDirty(spline); spline.points[index] = handleTransform.InverseTransformPoint(point); } } return point; } |
现在控制点以白色方形按钮的形式呈现出来了,但是按钮的大小有一点奇怪,要么太大要么太小。我们希望它和坐标轴一样保证相同的屏幕尺寸。HandleUtility.GetHandleSize方法可以提供固定的屏幕尺寸。
1 2 3 4 5 | float size = HandleUtility.GetHandleSize(point); Handles.color = Color.white; if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) { selectedIndex = index; } |