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

发表于2017-09-20
评论0 1.4k浏览

上一篇文章中介绍了直线的制作,下面继续曲线的制作流程,曲线算法很多,比如Bezier(贝赛尔曲线)和B Spine(B样条曲线)等。下面介绍Bezier曲线, 具体来说,我们将创建一个Beziér曲线。


Bezier曲线

Beziér曲线由点序列定义, 它从第一点开始,在最后一点结束,创建一个新的Bezier曲线组件并给它一个点数组, 还要给它一个Reset方法,它用三点进行初始化, 该方法也可用作特殊的Unity方法,该方法在创建或重置组件时由编辑器调用。代码如下所示:

  1. using UnityEngine;  
  2.   
  3. public class BezierCurve : MonoBehaviour {  
  4.   
  5.     public Vector3[] points;  
  6.   
  7.     public void Reset () {  
  8.         points = new Vector3[] {  
  9.             new Vector3(1f, 0f, 0f),  
  10.             new Vector3(2f, 0f, 0f),  
  11.             new Vector3(3f, 0f, 0f)  
  12.         };  
  13.     }  
  14. }  


继续可视化操作界面的编写,我们需要创建了一个基于LineInspector的曲线可视编辑, 为了减少代码重复,我们将显示点的代码移动到单独的ShowPoint方法,我们可以使用索引调用该方法,我们还将曲线,Handle转换和Handle旋转转换为类变量,因此我们不需要传递给ShowPoint。代码如下所示:

  1. using UnityEditor;  
  2. using UnityEngine;  
  3.   
  4. [CustomEditor(typeof(BezierCurve))]  
  5. public class BezierCurveInspector : Editor {  
  6.   
  7.     private BezierCurve curve;  
  8.     private Transform handleTransform;  
  9.     private Quaternion handleRotation;  
  10.   
  11.     private void OnSceneGUI () {  
  12.         curve = target as BezierCurve;  
  13.         handleTransform = curve.transform;  
  14.         handleRotation = Tools.pivotRotation == PivotRotation.Local ?  
  15.             handleTransform.rotation : Quaternion.identity;  
  16.   
  17.         Vector3 p0 = ShowPoint(0);  
  18.         Vector3 p1 = ShowPoint(1);  
  19.         Vector3 p2 = ShowPoint(2);  
  20.   
  21.         Handles.color = Color.white;  
  22.         Handles.DrawLine(p0, p1);  
  23.         Handles.DrawLine(p1, p2);  
  24.     }  
  25.   
  26.     private Vector3 ShowPoint (int index) {  
  27.         Vector3 point = handleTransform.TransformPoint(curve.points[index]);  
  28.         EditorGUI.BeginChangeCheck();  
  29.         point = Handles.DoPositionHandle(point, handleRotation);  
  30.         if (EditorGUI.EndChangeCheck()) {  
  31.             Undo.RecordObject(curve, "Move Point");  
  32.             EditorUtility.SetDirty(curve);  
  33.             curve.points[index] = handleTransform.InverseTransformPoint(point);  
  34.         }  
  35.         return point;  
  36.     }  
  37. }  

放到Unity中的效果图如下所示:



在编辑器中显示的效果:



给大家介绍一下:Beziér曲线的思想是它们是参数化的, 如果你给它一个值 - 通常叫做t - 在零和一之间,你会得到一个点在曲线上。 当t从零增加到1时,您从曲线的第一点移动到最后一点。
为了在场景中显示曲线,我们可以通过在曲线上的连续步长之间绘制直线来近似, 假设我们的曲线具有GetPoint方法,我们可以用简单的循环来做到这一点, 我们也不断绘制点之间的直线,但将其颜色变为灰色。代码如下:
  1. private const int lineSteps = 10;  
  2.               
  3.     private void OnSceneGUI () {  
  4.         curve = target as BezierCurve;  
  5.         handleTransform = curve.transform;  
  6.         handleRotation = Tools.pivotRotation == PivotRotation.Local ?  
  7.             handleTransform.rotation : Quaternion.identity;  
  8.   
  9.         Vector3 p0 = ShowPoint(0);  
  10.         Vector3 p1 = ShowPoint(1);  
  11.         Vector3 p2 = ShowPoint(2);  
  12.   
  13.         Handles.color = Color.gray;  
  14.         Handles.DrawLine(p0, p1);  
  15.         Handles.DrawLine(p1, p2);  
  16.   
  17.         Handles.color = Color.white;  
  18.         Vector3 lineStart = curve.GetPoint(0f);  
  19.         for (int i = 1; i <= lineSteps; i ) {  
  20.             Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);  
  21.             Handles.DrawLine(lineStart, lineEnd);  
  22.             lineStart = lineEnd;  
  23.         }  
  24.     }  

接下来,现在我们必须将GetPoint方法添加到Bezier曲线中,否则它将不会被编译。 这里我们再次做出一个假设,这次有一个实用的Beziér类可以对任何一个点进行计算, 我们把它们归结为一体,将结果转化为世界空间。

  1. public Vector3 GetPoint (float t) {  
  2.     return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], t));  
  3. }  

所以我们使用所需的方法添加一个静态Bezier类 现在,让我们忽略中间点,简单地在第一个和最后一个点之间进行线性内插。

  1. using UnityEngine;  
  2.   
  3. public static class Bezier {  
  4.   
  5.     public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {  
  6.         return Vector3.Lerp(p0, p2, t);  
  7.     }  
  8. }  

在Unity中的效果如下所示:





继续介绍,当然,终点之间的线性插值完全忽略了中点, 那么我们如何融入中点呢? 答案是多次插值。 首先,在第一个和中间点之间以及中间和最后一个点之间进行线性内插, 这给了我们两个新的点。 在这两个之间进行线性内插,给出了曲线上的最后一点。代码如下:

  1. public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {  
  2.     return Vector3.Lerp(Vector3.Lerp(p0, p1, t), Vector3.Lerp(p1, p2, t), t);  
  3. }  

效果如下:

这种曲线被称为二次Beziér曲线,因为涉及多项式数学。
线性曲线可以写为B(t) = (1 - t) P0   t P1
B(t) = (1 - t) ((1 - t) P0   t P1)  t ((1 - t) P1   t P2)更深一步。 这只是线性曲线,P0和P1被两条新的线性曲线取代。 它也可以被重写为更紧凑的形式B(t) = (1 - t)2 P0  2 (1 - tt P1   t2 P2
所以我们可以使用二次公式而不是三次调用Vector3.Lerp。

对应的代码如下:

[csharp] view plain copy
  1. public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, float t) {  
  2.     t = Mathf.Clamp01(t);  
  3.     float oneMinusT = 1f - t;  
  4.     return  
  5.         oneMinusT * oneMinusT * p0    
  6.         2f * oneMinusT * t * p1    
  7.         t * t * p2;  
  8. }  


另外,我们的二次Beziér曲线的一阶导数是B'(t) = 2 (1 - t) (P1 - P0) 2 t (P2 - P1)。 我们来补充一下:

[csharp] view plain copy
  1. public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, float t) {  
  2.     return  
  3.         2f * (1f - t) * (p1 - p0)    
  4.         2f * t * (p2 - p1);  
  5. }  


继续解释,该函数产生与曲线相切的线,可以将其解释为沿着曲线移动的速度。 所以现在我们可以添加一个GetVelocity方法到Bezier曲线。因为它产生一个速度矢量而不是一个点,它不应该受到曲线的位置的影响,所以我们在变换后减去它。
[csharp] view plain copy
  1. public Vector3 GetVelocity (float t) {  
  2.     return transform.TransformPoint(Bezier.GetFirstDerivative(points[0], points[1], points[2], t)) -  
  3.         transform.position;  
  4. }  

现在我们可以在Bezier曲线的Inspector的OnSceneGUI方法中可视化曲线的速度。

[csharp] view plain copy
  1. Vector3 lineStart = curve.GetPoint(0f);  
  2. Handles.color = Color.green;  
  3. Handles.DrawLine(lineStart, lineStart   curve.GetVelocity(0f));  
  4. for (int i = 1; i <= lineSteps; i ) {  
  5.     Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);  
  6.     Handles.color = Color.white;  
  7.     Handles.DrawLine(lineStart, lineEnd);  
  8.     Handles.color = Color.green;  
  9.     Handles.DrawLine(lineEnd, lineEnd   curve.GetVelocity(i / (float)lineSteps));  
  10.     lineStart = lineEnd;  
  11. }  



我们可以清楚地看到速度沿着曲线如何变化,但那些长长的线条使得视图混乱, 而不是显示速度,我们可以表现出运动的方向。

[csharp] view plain copy
  1. Vector3 lineStart = curve.GetPoint(0f);  
  2. Handles.color = Color.green;  
  3. Handles.DrawLine(lineStart, lineStart   curve.GetDirection(0f));  
  4. for (int i = 1; i <= lineSteps; i ) {  
  5.     Vector3 lineEnd = curve.GetPoint(i / (float)lineSteps);  
  6.     Handles.color = Color.white;  
  7.     Handles.DrawLine(lineStart, lineEnd);  
  8.     Handles.color = Color.green;  
  9.     Handles.DrawLine(lineEnd, lineEnd   curve.GetDirection(i / (float)lineSteps));  
  10.     lineStart = lineEnd;  
  11. }  

这需要我们将GetDirection添加到Bezier曲线,这简单地归一化速度。

[csharp] view plain copy
  1. public Vector3 GetDirection (float t) {  
  2.     return GetVelocity(t).normalized;  
  3. }  

我们再来一步,为Bezier添加新的方法,以获得立方曲线! 它的工作原理就像二次版本,除了它需要第四点,其公式进一步深入,导致六个线性插值的组合。 其合并函数变为B(t) = (1 - t)3 P0  3 (1 - t)2 t P1  3 (1 - t) t2 P2   t3 P3,其具有其一阶导数B'(t) = 3 (1 - t)2 (P1 - P0) 6 (1 - tt (P2 - P1) 3 t2 (P3 - P2)

[csharp] view plain copy
  1. public static Vector3 GetPoint (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {  
  2.     t = Mathf.Clamp01(t);  
  3.     float oneMinusT = 1f - t;  
  4.     return  
  5.         oneMinusT * oneMinusT * oneMinusT * p0    
  6.         3f * oneMinusT * oneMinusT * t * p1    
  7.         3f * oneMinusT * t * t * p2    
  8.         t * t * t * p3;  
  9. }  
  10.   
  11. public static Vector3 GetFirstDerivative (Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t) {  
  12.     t = Mathf.Clamp01(t);  
  13.     float oneMinusT = 1f - t;  
  14.     return  
  15.         3f * oneMinusT * oneMinusT * (p1 - p0)    
  16.         6f * oneMinusT * t * (p2 - p1)    
  17.         3f * t * t * (p3 - p2);  
  18. }  

因此,我们可以通过考虑另外一点来将Bezier曲线从二次方升级为立方, 确保将第四个点手动添加到其阵列中,或者通过重置该组件。代码如下:

[csharp] view plain copy
  1. public Vector3 GetPoint (float t) {  
  2.     return transform.TransformPoint(Bezier.GetPoint(points[0], points[1], points[2], points[3], t));  
  3. }  
  4.   
  5. public Vector3 GetVelocity (float t) {  
  6.     return transform.TransformPoint(  
  7.         Bezier.GetFirstDerivative(points[0], points[1], points[2], points[3], t)) - transform.position;  
  8. }  
  9.   
  10. public void Reset () {  
  11.     points = new Vector3[] {  
  12.         new Vector3(1f, 0f, 0f),  
  13.         new Vector3(2f, 0f, 0f),  
  14.         new Vector3(3f, 0f, 0f),  
  15.         new Vector3(4f, 0f, 0f)  
  16.     };  
  17. }  

现在,我们再增加一个点也就是四个点:

[csharp] view plain copy
  1. Vector3 p0 = ShowPoint(0);  
  2. Vector3 p1 = ShowPoint(1);  
  3. Vector3 p2 = ShowPoint(2);  
  4. Vector3 p3 = ShowPoint(3);  
  5.   
  6. Handles.color = Color.gray;  
  7. Handles.DrawLine(p0, p1);  
  8. Handles.DrawLine(p2, p3);  



给读者解释一下:现在很明显,我们用直线段绘制曲线,我们可以增加提高视觉品质的步骤,我们还可以使用迭代方法来精确到像素级。但我们也可以使用Unity的句柄,用DrawBezier方法,它负责为我们绘制漂亮的立方Bezier曲线。我们也可以用它们自己的方法来显示方向,并按比例缩小它们占用的空间。

[csharp] view plain copy
  1. private const float directionScale = 0.5f;  
  2.       
  3.     private void OnSceneGUI () {  
  4.         curve = target as BezierCurve;  
  5.         handleTransform = curve.transform;  
  6.         handleRotation = Tools.pivotRotation == PivotRotation.Local ?  
  7.             handleTransform.rotation : Quaternion.identity;  
  8.           
  9.         Vector3 p0 = ShowPoint(0);  
  10.         Vector3 p1 = ShowPoint(1);  
  11.         Vector3 p2 = ShowPoint(2);  
  12.         Vector3 p3 = ShowPoint(3);  
  13.           
  14.         Handles.color = Color.gray;  
  15.         Handles.DrawLine(p0, p1);  
  16.         Handles.DrawLine(p2, p3);  
  17.           
  18.         ShowDirections();  
  19.         Handles.DrawBezier(p0, p3, p1, p2, Color.white, null, 2f);  
  20.     }  
  21.   
  22.     private void ShowDirections () {  
  23.         Handles.color = Color.green;  
  24.         Vector3 point = curve.GetPoint(0f);  
  25.         Handles.DrawLine(point, point   curve.GetDirection(0f) * directionScale);  
  26.         for (int i = 1; i <= lineSteps; i ) {  
  27.             point = curve.GetPoint(i / (float)lineSteps);  
  28.             Handles.DrawLine(point, point   curve.GetDirection(i / (float)lineSteps) * directionScale);  
  29.         }  
  30.     }  


最终效果图如下所示:




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

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

0个评论