Unity中的曲线绘制(三)——样条曲线

发表于2016-11-02
评论0 1.01w浏览

  单独的一条贝塞尔曲线很容易生成,但是它并不能模拟更复杂的路径,但是我们可以通过组合不同的贝塞尔曲线,来得到复杂的曲线。复制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;
}
  由于我们使用了Array.Resize来新建一个更大的数组来存放新的点。它是System namespace中的内置方法,所以在脚本的开头要声明;
?
1
2
using UnityEngine;
using System;
  我们在spline的inspector里添加一个按钮,用于控制曲线的增加。我们可以重写Unity的OnInspectorGUI方法,来自定义自己的inspector。调用DrawDefaultInspector方法,然后用GUILayout方法绘制按钮,点击这个按钮可以为当前spline添加一段曲线。
?
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);
    }
}
  GUILayout.Button方法会绘制一个按钮并且返回它的点击状态


  但是场景中还是只能看见一条曲线,调整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();
}
  注意在OnInspectorGUI和OnSceneGUI里都分别引用了spline,这是因为这两个方法彼此独立,target可能会变化,所以最好分开声明一次。



  现在可以看到所有的曲线都被绘制出来了,但是只有第一条曲线上有速度的标识。这是因为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;
    }
}
  上面说到将t乘以曲线的段数,得到的整数部分是曲线的索引,那么减去这个整数部分,得到的小数部分就是在当前曲线上插值的系数。当t等于1时,直接认为是最后一段曲线即可。
?
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;
}
  现在在整条样条曲线上面都能看见速度的指示线了,为了保证每一段贝塞尔曲线的速度线的数量一致,将BezierSplineInspector.ShowDirections方法进行一些调整,利用BezierSpline.CurveCount来决定每一段曲线的速度绘制数量。
?
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;
}


腾讯GAD游戏程序交流群:484290331

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