Unity3D教你制作Bezier和Spine曲线编辑器四

发表于2017-09-19
评论0 966浏览
上篇文章给大家介绍了关于曲线编辑器的制作,下面开始讲如何去编辑,这就需要在曲线上加控制点虽然我们的样条是连续的,但它在曲线段之间会急剧的变化,这些突然变化也导致了点的方向和速度变化,因为两个曲线之间的共享控制点会产生两个不同的速度,每一个曲线都有一个速度。

如果我们想让曲线速度相等,必须确保定义它们的两个控制点——前一条曲线的三分之一和下一条曲线的第二点——在共享点周围镜像,这确保了第一和二阶导数是连续的。


其实,最灵活的方法是决定每条曲线的边界,当然,一旦我们有了这些限制,我们就不能让任何人直接编辑BezierSpline的点,因此,让我们将数组私有并提供对它的间接访问,确保让Unity知道我们仍然想要序列化我们的点,否则它们就不会被保存。代码如下所示:

  1. [SerializeField]  
  2.     private Vector3[] points;  
  3.   
  4.     public int ControlPointCount {  
  5.         get {  
  6.             return points.Length;  
  7.         }  
  8.     }  
  9.   
  10.     public Vector3 GetControlPoint (int index) {  
  11.         return points[index];  
  12.     }  
  13.   
  14.     public void SetControlPoint (int index, Vector3 point) {  
  15.         points[index] = point;  
  16.     }  

现在BezierSplineInspector必须使用新的方法和属性,而不是直接访问点数组。

  1. private void OnSceneGUI () {  
  2.         spline = target as BezierSpline;  
  3.         handleTransform = spline.transform;  
  4.         handleRotation = Tools.pivotRotation == PivotRotation.Local ?  
  5.             handleTransform.rotation : Quaternion.identity;  
  6.           
  7.         Vector3 p0 = ShowPoint(0);  
  8.         for (int i = 1; i < spline.ControlPointCount; i  = 3) {  
  9.             Vector3 p1 = ShowPoint(i);  
  10.             Vector3 p2 = ShowPoint(i   1);  
  11.             Vector3 p3 = ShowPoint(i   2);  
  12.               
  13.             Handles.color = Color.gray;  
  14.             Handles.DrawLine(p0, p1);  
  15.             Handles.DrawLine(p2, p3);  
  16.               
  17.             Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);  
  18.             p0 = p3;  
  19.         }  
  20.         ShowDirections();  
  21.     }  
  22.       
  23.     private Vector3 ShowPoint (int index) {  
  24.         Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));  
  25.         float size = HandleUtility.GetHandleSize(point);  
  26.         Handles.color = Color.white;  
  27.         if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {  
  28.             selectedIndex = index;  
  29.         }  
  30.         if (selectedIndex == index) {  
  31.             EditorGUI.BeginChangeCheck();  
  32.             point = Handles.DoPositionHandle(point, handleRotation);  
  33.             if (EditorGUI.EndChangeCheck()) {  
  34.                 Undo.RecordObject(spline, "Move Point");  
  35.                 EditorUtility.SetDirty(spline);  
  36.                 spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));  
  37.             }  
  38.         }  
  39.         return point;  
  40.     }  

当然 我们也不再希望允许直接访问检查器中的数组,因此取消对DrawDefaultInspector的调用,为了仍然允许通过输入进行更改,让我们为选定的点显示一个向量场。

  1. public override void OnInspectorGUI () {  
  2.         spline = target as BezierSpline;  
  3.         if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {  
  4.             DrawSelectedPointInspector();  
  5.         }  
  6.         if (GUILayout.Button("Add Curve")) {  
  7.             Undo.RecordObject(spline, "Add Curve");  
  8.             spline.AddCurve();  
  9.             EditorUtility.SetDirty(spline);  
  10.         }  
  11.     }  
  12.   
  13.     private void DrawSelectedPointInspector() {  
  14.         GUILayout.Label("Selected Point");  
  15.         EditorGUI.BeginChangeCheck();  
  16.         Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));  
  17.         if (EditorGUI.EndChangeCheck()) {  
  18.             Undo.RecordObject(spline, "Move Point");  
  19.             EditorUtility.SetDirty(spline);  
  20.             spline.SetControlPoint(selectedIndex, point);  
  21.         }  
  22.     }  

另外当我们在场景视图中选择一个点时,检查器不会刷新自己。我们可以通过调用spline的SetDirty来解决这个问题,但这并不正确,因为样条没有改变。幸运的是,我们可以发出重新绘制的请求。

  1. private Vector3 ShowPoint (int index) {  
  2.         Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));  
  3.         float size = HandleUtility.GetHandleSize(point);  
  4.         Handles.color = Color.white;  
  5.         if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {  
  6.             selectedIndex = index;  
  7.             Repaint();  
  8.         }  
  9.         if (selectedIndex == index) {  
  10.             EditorGUI.BeginChangeCheck();  
  11.             point = Handles.DoPositionHandle(point, handleRotation);  
  12.             if (EditorGUI.EndChangeCheck()) {  
  13.                 Undo.RecordObject(spline, "Move Point");  
  14.                 EditorUtility.SetDirty(spline);  
  15.                 spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));  
  16.             }  
  17.         }  
  18.         return point;  
  19.     }  


我们定义一个枚举类型来描述我们的三种模式。创建一个新脚本,删除默认代码,并使用三个选项定义enum。

  1. public enum BezierControlPointMode {  
  2.     Free,  
  3.     Aligned,  
  4.     Mirrored  
  5. }  

您需要重置您的样条或创建一个新的样条,以确保您有合适大小的数组。

[csharp] view plain copy
  1. <span style="white-space:pre">  </span>[SerializeField]  
  2.     private BezierControlPointMode[] modes;  
  3.       
  4.     public void AddCurve () {  
  5.         Vector3 point = points[points.Length - 1];  
  6.         Array.Resize(ref points, points.Length   3);  
  7.         point.x  = 1f;  
  8.         points[points.Length - 3] = point;  
  9.         point.x  = 1f;  
  10.         points[points.Length - 2] = point;  
  11.         point.x  = 1f;  
  12.         points[points.Length - 1] = point;  
  13.   
  14.         Array.Resize(ref modes, modes.Length   1);  
  15.         modes[modes.Length - 1] = modes[modes.Length - 2];  
  16.     }  
  17.       
  18.     public void Reset () {  
  19.         points = new Vector3[] {  
  20.             new Vector3(1f, 0f, 0f),  
  21.             new Vector3(2f, 0f, 0f),  
  22.             new Vector3(3f, 0f, 0f),  
  23.             new Vector3(4f, 0f, 0f)  
  24.         };  
  25.         modes = new BezierControlPointMode[] {  
  26.             BezierControlPointMode.Free,  
  27.             BezierControlPointMode.Free  
  28.         };  
  29.     }  

我们在曲线之间存储模式时,如果我们能得到和设置每个控制点的模式是很方便的。因此,我们需要将点索引转换为模式索引,因为在现实点

共享模式。作为一个例子,点索引序列0、1、2、3、4、5、6对应于模式索引序列0、0、1、1、1、2、2。所以我们需要加1然后除以3。


[csharp] view plain copy
  1. public BezierControlPointMode GetControlPointMode (int index) {  
  2.     return modes[(index   1) / 3];  
  3. }  
  4.   
  5. public void SetControlPointMode (int index, BezierControlPointMode mode) {  
  6.     modes[(index   1) / 3] = mode;  
  7. }  

现在,BezierSplineInspector可以允许我们改变所选点的模式,你会注意到,改变一个点的模式似乎也会改变与之相关的点的模式。

[csharp] view plain copy
  1. private void DrawSelectedPointInspector() {  
  2.         GUILayout.Label("Selected Point");  
  3.         EditorGUI.BeginChangeCheck();  
  4.         Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));  
  5.         if (EditorGUI.EndChangeCheck()) {  
  6.             Undo.RecordObject(spline, "Move Point");  
  7.             EditorUtility.SetDirty(spline);  
  8.             spline.SetControlPoint(selectedIndex, point);  
  9.         }  
  10.         EditorGUI.BeginChangeCheck();  
  11.         BezierControlPointMode mode = (BezierControlPointMode)  
  12.             EditorGUILayout.EnumPopup("Mode", spline.GetControlPointMode(selectedIndex));  
  13.         if (EditorGUI.EndChangeCheck()) {  
  14.             Undo.RecordObject(spline, "Change Point Mode");  
  15.             spline.SetControlPointMode(selectedIndex, mode);  
  16.             EditorUtility.SetDirty(spline);  
  17.         }  
  18.     }  

如果我们在场景视图中得到一些关于节点类型的可视化反馈,那将是很有用的,我们可以很容易地把这些点涂上颜色,我将用白色表示自由,黄色表示对齐。

[csharp] view plain copy
  1. private static Color[] modeColors = {  
  2.         Color.white,  
  3.         Color.yellow,  
  4.         Color.cyan  
  5.     };  
  6.       
  7.     private Vector3 ShowPoint (int index) {  
  8.         Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));  
  9.         float size = HandleUtility.GetHandleSize(point);  
  10.         Handles.color = modeColors[(int)spline.GetControlPointMode(index)];  
  11.         if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {  
  12.             selectedIndex = index;  
  13.             Repaint();  
  14.         }  
  15.         if (selectedIndex == index) {  
  16.             EditorGUI.BeginChangeCheck();  
  17.             point = Handles.DoPositionHandle(point, handleRotation);  
  18.             if (EditorGUI.EndChangeCheck()) {  
  19.                 Undo.RecordObject(spline, "Move Point");  
  20.                 EditorUtility.SetDirty(spline);  
  21.                 spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));  
  22.             }  
  23.         }  
  24.         return point;  
  25.     }  



到目前为止,我们只是着色点,现在是执行约束的时候了,我们为BezierSpline添加一个新的方法,当一个点移动或模式发生改变时调用它,它需要一个点索引,并从检索相关模式开始。

[csharp] view plain copy
  1. public void SetControlPoint (int index, Vector3 point) {  
  2.         points[index] = point;  
  3.         EnforceMode(index);  
  4.     }  
  5.       
  6.     public void SetControlPointMode (int index, BezierControlPointMode mode) {  
  7.         modes[(index   1) / 3] = mode;  
  8.         EnforceMode(index);  
  9.     }  
  10.   
  11.     private void EnforceMode (int index) {  
  12.         int modeIndex = (index   1) / 3;  
  13.     }  

我们应该检查是否真的没有强制执行任何东西。当模式被设置为自由时,或者当我们在曲线的端点时。在这些情况下,我们可以在不做任何事情的情况下返回。

[csharp] view plain copy
  1. private void EnforceMode (int index) {  
  2.     int modeIndex = (index   1) / 3;  
  3.     BezierControlPointMode mode = modes[modeIndex];  
  4.     if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {  
  5.         return;  
  6.     }  
  7. }  

现在 我们应该调整哪一点?当我们改变一个点的模式时,它要么是曲线上的一点,要么是它的一个相邻的点。当我们选择中间点时,我们可以保持之前的点固定并在对边的点上执行约束。如果我们选择了另一个点,我们应该保持一个固定和调整它的反向。这样我们选择的点总是保持在它所在的位置,我们来定义这些点的指数。

[csharp] view plain copy
  1. if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {  
  2.             return;  
  3.         }  
  4.           
  5.         int middleIndex = modeIndex * 3;  
  6.         int fixedIndex, enforcedIndex;  
  7.         if (index <= middleIndex) {  
  8.             fixedIndex = middleIndex - 1;  
  9.             enforcedIndex = middleIndex   1;  
  10.         }  
  11.         else {  
  12.             fixedIndex = middleIndex   1;  
  13.             enforcedIndex = middleIndex - 1;  
  14.         }  

我们首先考虑镜像的情况,为了在中间点镜像,我们必须从中间的向量取到固定的点——它是(固定的-中)-并把它倒过来,这是切线,

把它加到中间,得到了我们的执行点。

[csharp] view plain copy
  1. if (index <= middleIndex) {  
  2.             fixedIndex = middleIndex - 1;  
  3.             enforcedIndex = middleIndex   1;  
  4.         }  
  5.         else {  
  6.             fixedIndex = middleIndex   1;  
  7.             enforcedIndex = middleIndex - 1;  
  8.         }  
  9.   
  10.         Vector3 middle = points[middleIndex];  
  11.         Vector3 enforcedTangent = middle - points[fixedIndex];  
  12.         points[enforcedIndex] = middle   enforcedTangent;  

对齐模式,我们还必须确保新切线的长度与以前的相同,我们把它标准化,然后乘以中间的和以前的控制点之间的距离。

[csharp] view plain copy
  1. Vector3 enforcedTangent = middle - points[fixedIndex];  
  2.         if (mode == BezierControlPointMode.Aligned) {  
  3.             enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);  
  4.         }  
  5.         points[enforcedIndex] = middle   enforcedTangent;  

终于看到效果了,从现在开始,每当你移动一个点或改变点的模式时,约束就会被执行,但当移动中间点时,前一点总是固定不变,下一个点总是被强制执行,这可能很好,但如果两个点都和中间的点一起移动的话,这是很直观的,我们来调整SetControlPoint把它们移动到一起。

[csharp] view plain copy
  1. public void SetControlPoint (int index, Vector3 point) {  
  2.     if (index % 3 == 0) {  
  3.         Vector3 delta = point - points[index];  
  4.         if (index > 0) {  
  5.             points[index - 1]  = delta;  
  6.         }  
  7.         if (index   1 < points.Length) {  
  8.             points[index   1]  = delta;  
  9.         }  
  10.     }  
  11.     points[index] = point;  
  12.     EnforceMode(index);  
  13. }  

还应该确保在添加曲线时,约束被执行,可以通过在添加新曲线的地方调用强制执行程序来实现这一点。

[csharp] view plain copy
  1. public void AddCurve () {  
  2.     Vector3 point = points[points.Length - 1];  
  3.     Array.Resize(ref points, points.Length   3);  
  4.     point.x  = 1f;  
  5.     points[points.Length - 3] = point;  
  6.     point.x  = 1f;  
  7.     points[points.Length - 2] = point;  
  8.     point.x  = 1f;  
  9.     points[points.Length - 1] = point;  
  10.   
  11.     Array.Resize(ref modes, modes.Length   1);  
  12.     modes[modes.Length - 1] = modes[modes.Length - 2];  
  13.     EnforceMode(points.Length - 4);  
  14. }  

我们 还可以添加另一个约束,通过强制第一个和最后一个控制点共享相同的位置,我们可以将样条变成一个循环。当然,我们也必须考虑模式。

我们将一个循环属性添加到BezierSpline。当它设置为true时,我们确保端点匹配的模式和我们调用SetPosition,相信它会处理位置和模式约束。

[csharp] view plain copy
  1. [SerializeField]  
  2.     private bool loop;  
  3.   
  4.     public bool Loop {  
  5.         get {  
  6.             return loop;  
  7.         }  
  8.         set {  
  9.             loop = value;  
  10.             if (value == true) {  
  11.                 modes[modes.Length - 1] = modes[0];  
  12.                 SetControlPoint(0, points[0]);  
  13.             }  
  14.         }  
  15.     }  

现在我们可以将循环属性添加到BezierSplineInspector:

[csharp] view plain copy
  1. public override void OnInspectorGUI () {  
  2.         spline = target as BezierSpline;  
  3.         EditorGUI.BeginChangeCheck();  
  4.         bool loop = EditorGUILayout.Toggle("Loop", spline.Loop);  
  5.         if (EditorGUI.EndChangeCheck()) {  
  6.             Undo.RecordObject(spline, "Toggle Loop");  
  7.             EditorUtility.SetDirty(spline);  
  8.             spline.Loop = loop;  
  9.         }  
  10.         if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {  
  11.             DrawSelectedPointInspector();  
  12.         }  
  13.         if (GUILayout.Button("Add Curve")) {  
  14.             Undo.RecordObject(spline, "Add Curve");  
  15.             spline.AddCurve();  
  16.             EditorUtility.SetDirty(spline);  
  17.         }  
  18.     }  


正确地执行循环,我们需要对BezierSpline做更多的更改。

首先,SetControlPointMode需要确保在循环的情况下,第一个和最后一个模式保持相等。

[csharp] view plain copy
  1. public void SetControlPointMode (int index, BezierControlPointMode mode) {  
  2.     int modeIndex = (index   1) / 3;  
  3.     modes[modeIndex] = mode;  
  4.     if (loop) {  
  5.         if (modeIndex == 0) {  
  6.             modes[modes.Length - 1] = mode;  
  7.         }  
  8.         else if (modeIndex == modes.Length - 1) {  
  9.             modes[0] = mode;  
  10.         }  
  11.     }  
  12.     EnforceMode(index);  
  13. }  

其次在处理一个循环时,SetControlPoint需要不同的边界情况。

[csharp] view plain copy
  1. public void SetControlPoint (int index, Vector3 point) {  
  2.     if (index % 3 == 0) {  
  3.         Vector3 delta = point - points[index];  
  4.         if (loop) {  
  5.             if (index == 0) {  
  6.                 points[1]  = delta;  
  7.                 points[points.Length - 2]  = delta;  
  8.                 points[points.Length - 1] = point;  
  9.             }  
  10.             else if (index == points.Length - 1) {  
  11.                 points[0] = point;  
  12.                 points[1]  = delta;  
  13.                 points[index - 1]  = delta;  
  14.             }  
  15.             else {  
  16.                 points[index - 1]  = delta;  
  17.                 points[index   1]  = delta;  
  18.             }  
  19.         }  
  20.         else {  
  21.             if (index > 0) {  
  22.                 points[index - 1]  = delta;  
  23.             }  
  24.             if (index   1 < points.Length) {  
  25.                 points[index   1]  = delta;  
  26.             }  
  27.         }  
  28.     }  
  29.     points[index] = point;  
  30.     EnforceMode(index);  
  31. }  


再次,EnforceMode函数中,强制执行的只能是在最后的时候,它还必须检查固定的或强制的点:

[csharp] view plain copy
  1. private void EnforceMode (int index) {  
  2.         int modeIndex = (index   1) / 3;  
  3.         BezierControlPointMode mode = modes[modeIndex];  
  4.         if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Length - 1)) {  
  5.             return;  
  6.         }  
  7.   
  8.         int middleIndex = modeIndex * 3;  
  9.         int fixedIndex, enforcedIndex;  
  10.         if (index <= middleIndex) {  
  11.             fixedIndex = middleIndex - 1;  
  12.             if (fixedIndex < 0) {  
  13.                 fixedIndex = points.Length - 2;  
  14.             }  
  15.             enforcedIndex = middleIndex   1;  
  16.             if (enforcedIndex >= points.Length) {  
  17.                 enforcedIndex = 1;  
  18.             }  
  19.         }  
  20.         else {  
  21.             fixedIndex = middleIndex   1;  
  22.             if (fixedIndex >= points.Length) {  
  23.                 fixedIndex = 1;  
  24.             }  
  25.             enforcedIndex = middleIndex - 1;  
  26.             if (enforcedIndex < 0) {  
  27.                 enforcedIndex = points.Length - 2;  
  28.             }  
  29.         }  
  30.   
  31.         Vector3 middle = points[middleIndex];  
  32.         Vector3 enforcedTangent = middle - points[fixedIndex];  
  33.         if (mode == BezierControlPointMode.Aligned) {  
  34.             enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);  
  35.         }  
  36.         points[enforcedIndex] = middle   enforcedTangent;  
  37.     }  

最后我们还需要在向样条添加曲线时考虑循环:

[csharp] view plain copy
  1. public void AddCurve () {  
  2.         Vector3 point = points[points.Length - 1];  
  3.         Array.Resize(ref points, points.Length   3);  
  4.         point.x  = 1f;  
  5.         points[points.Length - 3] = point;  
  6.         point.x  = 1f;  
  7.         points[points.Length - 2] = point;  
  8.         point.x  = 1f;  
  9.         points[points.Length - 1] = point;  
  10.   
  11.         Array.Resize(ref modes, modes.Length   1);  
  12.         modes[modes.Length - 1] = modes[modes.Length - 2];  
  13.         EnforceMode(points.Length - 4);  
  14.   
  15.         if (loop) {  
  16.             points[points.Length - 1] = points[0];  
  17.             modes[modes.Length - 1] = modes[0];  
  18.             EnforceMode(0);  
  19.         }  
  20.     }  

我们有循环了,但我们不能再看到样条从哪里开始,这很不方便,我们可以通过让BezierSplineInspector总是将第一点的点的大小增加一倍来说明这一点。

注意,如果在一个循环中,最后一点会被绘制在上面,所以如果你点击了大圆点的中间,你会选择最后一点,而如果你从中间点击了,你会得到第一个点。

[csharp] view plain copy
  1. private Vector3 ShowPoint (int index) {  
  2.         Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));  
  3.         float size = HandleUtility.GetHandleSize(point);  
  4.         if (index == 0) {  
  5.             size *= 2f;  
  6.         }  
  7.         Handles.color = modeColors[(int)spline.GetControlPointMode(index)];  
  8.         if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {  
  9.             selectedIndex = index;  
  10.             Repaint();  
  11.         }  
  12.         if (selectedIndex == index) {  
  13.             EditorGUI.BeginChangeCheck();  
  14.             point = Handles.DoPositionHandle(point, handleRotation);  
  15.             if (EditorGUI.EndChangeCheck()) {  
  16.                 Undo.RecordObject(spline, "Move Point");  
  17.                 EditorUtility.SetDirty(spline);  
  18.                 spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));  
  19.             }  
  20.         }  
  21.         return point;  
  22.     }  

效果如下:


代码下载地址:链接:http://pan.baidu.com/s/1gfrJVrl  密码:ft1o 中的编号04的包。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引