Unity中的曲线绘制(四)——约束控制点

发表于2016-10-31
评论0 3.6k浏览
导语
       对样条曲线做出一些条件约束,两条贝塞尔曲线的衔接点处切线要连续,移动衔接点时相邻点也要同步移动。还可以设置条件属性来实现曲线成环,首尾相连。

约束控制点
       尽管我们的样条曲线是连续的,但是在衔接点(第一条曲线的最后一个点/第二条曲线的第一个点),方向会发生突变。这是因为在这个点,两条曲线有不同方向的速度,所以会出现转折。
       如果我们希望在衔接点## 约束控制点
       尽管我们的样条曲线是连续的,但是在共享的控制点(第一条曲线的最后一个点/第二条曲线的第一个点),方向会发生突变。这是因为在这个点,两条曲线有不同方向的速度,所以会出现转折。
如果我们希望在衔接点两条曲线的速度一致,那么该点的前一个点和后一个点(第一条曲线的倒数第二个点和第二条曲线的第二个点)应当和衔接点在同一条线上。如下图,S点就是两条贝塞尔曲线共享的控制点,由于AS和SB方向不同,导致样条曲线在S点发生突变,如果A、S、B共线,那么曲线在S点就是平滑的。


       当然,只需要保证A、S、B共线,但是AS和SB的距离不需要相同,这样一来,尽管速度的数值会突变,但是方向仍是连续的。这种情况下,曲线的一阶导是连续的,而二阶导则不连续。
      最灵活的方式是为每条贝塞尔曲线划定一个约束范围。这样一来,我们不能直接让BezierSpline.points被修改,将其改为private,另外提供接口来获取它们。记得添加SerializeField前缀,保证Unity能保存它的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[SerializeField]
private Vector3[] points;
 
public int ControlPointCount {
    get {
        return points.Length;
    }
}
 
public Vector3 GetControlPoint (int index) {
    return points[index];
}
 
public void SetControlPoint (int index, Vector3 point) {
    points[index] = point;
}
BezierSplineInspector中的获取控制点相关代码要进行调整。
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
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.ControlPointCount; 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();
}
 
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    Handles.color = Color.white;
    if (Handles.Button(point, handleRotation, size * handleSize, size * 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.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
       既然将控制点封装了,我们就不能在默认的Inspector中修改它们的位置了,但是我们可以自定义一个绘制方法,用于输入数值修改选中的控制点的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public override void OnInspectorGUI () {
    spline = target as BezierSpline;
    if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
        DrawSelectedPointInspector();
    }
    if (GUILayout.Button("Add Curve")) {
        Undo.RecordObject(spline, "Add Curve");
        spline.AddCurve();
        EditorUtility.SetDirty(spline);
    }
}
 
private void DrawSelectedPointInspector() {
    GUILayout.Label("Selected Point");
    EditorGUI.BeginChangeCheck();
    Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Move Point");
        EditorUtility.SetDirty(spline);
        spline.SetControlPoint(selectedIndex, point);
    }
      但是,当我们选中一个点,但是不去拖动它的话,Inspector中并不会刷新,应当在选中点时重新绘制Inspector。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    Handles.color = Color.white;
    if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
        selectedIndex = index;
        Repaint();
    }
    if (selectedIndex == index) {
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck()) {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}


      定义一个枚举类型,用于描述约束的几种方式。
1
2
3
4
5
public enum BezierControlPointMode {
    Free,
    Aligned,
    Mirrored
}
       Now we can add these modes to BezierSpline. We only need to store the mode in between curves, so let’s put them in an array with a length equal to the number of curves plus one. You’ll need to reset your spline or create a new one to make sure you have an array of the right size.
       在BezierSpline中使用此枚举。将它存储在数组中,长度为曲线的个数加一。记得在Unity中reset一下相关对象。
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
[SerializeField]
private BezierControlPointMode[] modes;
 
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(ref modes, modes.Length + 1);
    modes[modes.Length - 1] = modes[modes.Length - 2];
}
 
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)
    };
    modes = new BezierControlPointMode[] {
        BezierControlPointMode.Free,
        BezierControlPointMode.Free
    };
}
      为每个控制点提供简便的设置/获取mode方法。初始状态下,只有一条贝塞尔曲线,4个点,两个mode,那么点与modes数组的对应关系就是0-0,1-0,2-1,3-1;推广开来,对于样条曲线,0,1,2,3,4,5,6对应modes数组的索引是0,0,1,1,1,2,2,由于点3为衔接点,所以以它为界,分别代表0,0,1,1,和1,1,2,2两条贝塞尔曲线。
1
2
3
4
5
6
7
public BezierControlPointMode GetControlPointMode (int index) {
    return modes[(index + 1) / 3];
}
 
public void SetControlPointMode (int index, BezierControlPointMode mode) {
    modes[(index + 1) / 3] = mode;
}
     为BezierSplineInspector添加功能,当选中控制点时,可以在Inspector中修改该点的mode以及与该点关联的mode。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void DrawSelectedPointInspector() {
    GUILayout.Label("Selected Point");
    EditorGUI.BeginChangeCheck();
    Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Move Point");
        EditorUtility.SetDirty(spline);
        spline.SetControlPoint(selectedIndex, point);
    }
    EditorGUI.BeginChangeCheck();
    BezierControlPointMode mode = (BezierControlPointMode)
        EditorGUILayout.EnumPopup("Mode", spline.GetControlPointMode(selectedIndex));
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Change Point Mode");
        spline.SetControlPointMode(selectedIndex, mode);
        EditorUtility.SetDirty(spline);
    }
}


     为控制点修改颜色,来区分控制点的mode。
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
private static Color[] modeColors = {
    Color.white,
    Color.yellow,
    Color.cyan
};
 
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
    if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
        selectedIndex = index;
        Repaint();
    }
    if (selectedIndex == index) {
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck()) {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}


     下面来对控制点进行约束,为BezierSpline添加新方法EnforceMode,当控制点的位置或者mode发生修改时调用。需要传入控制点的索引,首先转化为对应的modes数组索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
public void SetControlPoint (int index, Vector3 point) {
    points[index] = point;
    EnforceMode(index);
}
 
public void SetControlPointMode (int index, BezierControlPointMode mode) {
    modes[(index + 1) / 3] = mode;
    EnforceMode(index);
}
 
private void EnforceMode (int index) {
    int modeIndex = (index + 1) / 3;
}
        判别是否需要约束,当该点的mode为Free时,或者处理对象是曲线的第一个点或者最后一个点,那么不用做任何处理,直接返回
1
2
3
4
5
6
7
private void EnforceMode (int index) {
    int modeIndex = (index + 1) / 3;
    BezierControlPointMode mode = modes[modeIndex];
    if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
        return;
    }
}
        当目标点是两条贝塞尔曲线的交点,及其相邻点时,我们要对其进行约束。选中中间点时,保持它上一个点固定不动,对下一个点进行处理。而选中中间点的相邻点时,对其对面的点进行约束,也就是说,选中的那个点是永远固定的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
== modes.Length - 1) {
        return;
    }
 
    int middleIndex = modeIndex * 3;// 得到modelIndex对应的三个点中的中间点
    int fixedIndex, enforcedIndex;
    if (index <= middleIndex) {
        fixedIndex = middleIndex - 1;
        enforcedIndex = middleIndex + 1;
    }
    else {
        fixedIndex = middleIndex + 1;
        enforcedIndex = middleIndex - 1;
    }
       考虑mirror(镜像)情况,即约束点和固定点关于中间点对称,得到固定点到中间点向量,在固定点上加上这个向量,就能得到约束点修正后的位置。如下图,当选中B时,需要约束的点是A,约束后它的位置应当在A’处,A’与B关于S对称。


1
2
3
4
5
6
7
8
9
10
11
12
if (index <= middleIndex) {
    fixedIndex = middleIndex - 1;
    enforcedIndex = middleIndex + 1;
}
else {
    fixedIndex = middleIndex + 1;
    enforcedIndex = middleIndex - 1;
}
 
Vector3 middle = points[middleIndex];
Vector3 enforcedTangent = middle - points[fixedIndex];
points[enforcedIndex] = middle + enforcedTangent;
        考虑aligned情况,在约束点与固定点中间点共线的前提下,还要保证它与中间点之间的距离不发生改变。因此将约束方向标准化,乘以原来的长度,就是目标约束向量。
1
2
3
4
5
Vector3 enforcedTangent = middle - points[fixedIndex];
if (mode == BezierControlPointMode.Aligned) {
    enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
}
points[enforcedIndex] = middle + enforcedTangent;


       这样一来,当移动控制点时,约束就会被调用,但是由于之前的设计,当移动中间点时,它的前一个点永远是固定的,而后一个点会被约束。如果两个点都随着中间点移动的话,会更加直观一些,因此调整SetControlPoint方法,以达到此目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
public void SetControlPoint (int index, Vector3 point) {
    if (index % 3 == 0) {
        Vector3 delta = point - points[index];
        if (index > 0) {
            points[index - 1] += delta;
        }
        if (index + 1 < points.Length) {
            points[index + 1] += delta;
        }
    }
    points[index] = point;
    EnforceMode(index);
}
        To wrap things up, we should also make sure that the constraints are enforced when we add a curve. We can do this by simply calling EnforceMode at the point where the new curve was added.
        当添加新曲线的时候,也应当施加约束(此时原来曲线的末尾也变成了中间点),做法很简单,只要在AddCurve的最后调用EnforceMode就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(ref modes, modes.Length + 1);
    modes[modes.Length - 1] = modes[modes.Length - 2];
    EnforceMode(points.Length - 4);
}
        除了强制两条贝塞尔曲线在交点连续的约束外,我们还可以对样条曲线的起点和终点进行约束,使这两个点重合。
       为BezierSpline添加属性loop,当其为true时,我们将首尾两点的mode统一,并且调用SetPosition,它中间已经实现了位置与mode的约束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[SerializeField]
private bool loop;
 
public bool Loop {
    get {
        return loop;
    }
    set {
        loop = value;
        if (value == true) {
            modes[modes.Length - 1] = modes[0];
            SetControlPoint(0, points[0]);
        }
    }
}
       在BezierSplineInspector中使用loop属性,这样在Unity的Inspector里可以勾选,决定曲线是否首尾相连。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override void OnInspectorGUI () {
    spline = target as BezierSpline;
    EditorGUI.BeginChangeCheck();
    bool loop = EditorGUILayout.Toggle("Loop", spline.Loop);
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Toggle Loop");
        EditorUtility.SetDirty(spline);
        spline.Loop = loop;
    }
    if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
        DrawSelectedPointInspector();
    }
    if (GUILayout.Button("Add Curve")) {
        Undo.RecordObject(spline, "Add Curve");
        spline.AddCurve();
        EditorUtility.SetDirty(spline);
    }
}


     为了正确实现循环曲线,我们还需要对BezierSpline进行修改,在 SetControlPointMode中,如果loop为true,那么当首/尾中的点的mode修改时,另一点的mode也要修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    int modeIndex = (index + 1) / 3;
    modes[modeIndex] = mode;
    if (loop) {
        if (modeIndex == 0) {
            modes[modes.Length - 1] = mode;
        }
        else if (modeIndex == modes.Length - 1) {
            modes[0] = mode;
        }
    }
    EnforceMode(index);
}
      SetControlPoint也需要进行修改。结合loop的值,对传入的index进行多次判断
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 void SetControlPoint (int index, Vector3 point) {
    if (index % 3 == 0) {
        Vector3 delta = point - points[index];
        if (loop) {
            if (index == 0) {
                points[1] += delta;
                points[points.Length - 2] += delta;
                points[points.Length - 1] = point;
            }
            else if (index == points.Length - 1) {
                points[0] = point;
                points[1] += delta;
                points[index - 1] += delta;
            }
            else {
                points[index - 1] += delta;
                points[index + 1] += delta;
            }
        }
        else {
            if (index > 0) {
                points[index - 1] += delta;
            }
            if (index + 1 < points.Length) {
                points[index + 1] += delta;
            }
        }
    }
    points[index] = point;
    EnforceMode(index);
}
      此时EnforceMode方法也要做出相应的修改,来处理循环曲线的情况,本来对于首尾两个点是不作约束的,但是如果是loop为true的话,首尾点会变成一个中间点。
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
private void EnforceMode (int index) {
    int modeIndex = (index + 1) / 3;
    BezierControlPointMode mode = modes[modeIndex];
    if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Length - 1)) {
        return;
    }
 
    int middleIndex = modeIndex * 3;
    int fixedIndex, enforcedIndex;
    if (index <= middleIndex) {
        fixedIndex = middleIndex - 1;
        if (fixedIndex < 0) {
            fixedIndex = points.Length - 2;
        }
        enforcedIndex = middleIndex + 1;
        if (enforcedIndex >= points.Length) {
            enforcedIndex = 1;
        }
    }
    else {
        fixedIndex = middleIndex + 1;
        if (fixedIndex >= points.Length) {
            fixedIndex = 1;
        }
        enforcedIndex = middleIndex - 1;
        if (enforcedIndex < 0) {
            enforcedIndex = points.Length - 2;
        }
    }
 
    Vector3 middle = points[middleIndex];
    Vector3 enforcedTangent = middle - points[fixedIndex];
    if (mode == BezierControlPointMode.Aligned) {
        enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
    }
    points[enforcedIndex] = middle + enforcedTangent;
}
     最后,在AddCurve中也要进行loop的情况处理,添加新曲线的时候,首尾也要视情况相连。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(ref modes, modes.Length + 1);
    modes[modes.Length - 1] = modes[modes.Length - 2];
    EnforceMode(points.Length - 4);
 
    if (loop) {
        points[points.Length - 1] = points[0];
        modes[modes.Length - 1] = modes[0];
        EnforceMode(0);
    }
}


      现在可以通过勾选loop选项,使得我们的样条曲线形成首尾相接的环,但是有个问题,就是我们看不出来初始的起点在哪里了,我们可以在BezierSplineInspector中将起点的图示size变大,便于显示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    if (index == 0) {
        size *= 2f;// 两倍大小
    }
    Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
    if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
        selectedIndex = index;
        Repaint();
    }
    if (selectedIndex == index) {
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck()) {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}


       两条曲线的速度一致,那么该点的前一个点和后一个点(第一条曲线的倒数第二个点和第二条曲线的第二个点)应当和衔接点在同一条线上。如下图,S点就是两条贝塞尔曲线共享的控制点,由于AS和SB方向不同,导致样条曲线在S点发生突变,如果A、S、B共线,那么曲线在S点就是平滑的。


       当然,只需要保证A、S、B共线,但是AS和SB的距离不需要相同,这样一来,尽管速度的数值会突变,但是方向仍是连续的。这种情况下,曲线的一阶导是连续的,而二阶导则不连续。
       最灵活的方式是为每条贝塞尔曲线划定一个约束范围。这样一来,我们不能直接让BezierSpline.points被修改,将其改为private,另外提供接口来获取它们。记得添加SerializeField前缀,保证Unity能保存它的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[SerializeField]
private Vector3[] points;
 
public int ControlPointCount {
    get {
        return points.Length;
    }
}
 
public Vector3 GetControlPoint (int index) {
    return points[index];
}
 
public void SetControlPoint (int index, Vector3 point) {
    points[index] = point;
}
     BezierSplineInspector中的获取控制点相关代码要进行调整。
 
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
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.ControlPointCount; 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();
}
 
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    Handles.color = Color.white;
    if (Handles.Button(point, handleRotation, size * handleSize, size * 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.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}
       既然将控制点封装了,我们就不能在默认的Inspector中修改它们的位置了,但是我们可以自定义一个绘制方法,用于输入数值修改选中的控制点的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public override void OnInspectorGUI () {
    spline = target as BezierSpline;
    if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
        DrawSelectedPointInspector();
    }
    if (GUILayout.Button("Add Curve")) {
        Undo.RecordObject(spline, "Add Curve");
        spline.AddCurve();
        EditorUtility.SetDirty(spline);
    }
}
 
private void DrawSelectedPointInspector() {
    GUILayout.Label("Selected Point");
    EditorGUI.BeginChangeCheck();
    Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Move Point");
        EditorUtility.SetDirty(spline);
        spline.SetControlPoint(selectedIndex, point);
    }
       但是,当我们选中一个点,但是不去拖动它的话,Inspector中并不会刷新,应当在选中点时重新绘制Inspector。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ize, Handles.DotCap)) {
        selectedIndex = index;
        Repaint();
    }
    if (selectedIndex == index) {
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck()) {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}


     定义一个枚举类型,用于描述约束的几种方式
1
2
3
4
5
public enum BezierControlPointMode {
    Free,
    Aligned,
    Mirrored
}
        Now we can add these modes to BezierSpline. We only need to store the mode in between curves, so let’s put them in an array with a length equal to the number of curves plus one. You’ll need to reset your spline or create a new one to make sure you have an array of the right size.
      在BezierSpline中使用此枚举。将它存储在数组中,长度为曲线的个数加一。记得在Unity中reset一下相关对象。
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
[SerializeField]
private BezierControlPointMode[] modes;
 
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(ref modes, modes.Length + 1);
    modes[modes.Length - 1] = modes[modes.Length - 2];
}
 
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)
    };
    modes = new BezierControlPointMode[] {
        BezierControlPointMode.Free,
        BezierControlPointMode.Free
    };
}
        为每个控制点提供简便的设置/获取mode方法。初始状态下,只有一条贝塞尔曲线,4个点,两个mode,那么点与modes数组的对应关系就是0-0,1-0,2-1,3-1;推广开来,对于样条曲线,0,1,2,3,4,5,6对应modes数组的索引是0,0,1,1,1,2,2,由于点3为衔接点,所以以它为界,分别代表0,0,1,1,和1,1,2,2两条贝塞尔曲线。
1
2
3
4
5
6
7
public BezierControlPointMode GetControlPointMode (int index) {
    return modes[(index + 1) / 3];
}
 
public void SetControlPointMode (int index, BezierControlPointMode mode) {
    modes[(index + 1) / 3] = mode;
}
       为BezierSplineInspector添加功能,当选中控制点时,可以在Inspector中修改该点的mode以及与该点关联的mode。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void DrawSelectedPointInspector() {
    GUILayout.Label("Selected Point");
    EditorGUI.BeginChangeCheck();
    Vector3 point = EditorGUILayout.Vector3Field("Position", spline.GetControlPoint(selectedIndex));
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Move Point");
        EditorUtility.SetDirty(spline);
        spline.SetControlPoint(selectedIndex, point);
    }
    EditorGUI.BeginChangeCheck();
    BezierControlPointMode mode = (BezierControlPointMode)
        EditorGUILayout.EnumPopup("Mode", spline.GetControlPointMode(selectedIndex));
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Change Point Mode");
        spline.SetControlPointMode(selectedIndex, mode);
        EditorUtility.SetDirty(spline);
    }
}


   为控制点修改颜色,来区分控制点的mode。
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
private static Color[] modeColors = {
    Color.white,
    Color.yellow,
    Color.cyan
};
 
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
    if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
        selectedIndex = index;
        Repaint();
    }
    if (selectedIndex == index) {
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck()) {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}


       下面来对控制点进行约束,为BezierSpline添加新方法EnforceMode,当控制点的位置或者mode发生修改时调用。需要传入控制点的索引,首先转化为对应的modes数组索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
public void SetControlPoint (int index, Vector3 point) {
    points[index] = point;
    EnforceMode(index);
}
 
public void SetControlPointMode (int index, BezierControlPointMode mode) {
    modes[(index + 1) / 3] = mode;
    EnforceMode(index);
}
 
private void EnforceMode (int index) {
    int modeIndex = (index + 1) / 3;
}
      判别是否需要约束,当该点的mode为Free时,或者处理对象是曲线的第一个点或者最后一个点,那么不用做任何处理,直接返回
1
2
3
4
5
6
7
private void EnforceMode (int index) {
    int modeIndex = (index + 1) / 3;
    BezierControlPointMode mode = modes[modeIndex];
    if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
        return;
    }
}
       当目标点是两条贝塞尔曲线的交点,及其相邻点时,我们要对其进行约束。选中中间点时,保持它上一个点固定不动,对下一个点进行处理。而选中中间点的相邻点时,对其对面的点进行约束,也就是说,选中的那个点是永远固定的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
    return;
}
 
int middleIndex = modeIndex * 3;// 得到modelIndex对应的三个点中的中间点
int fixedIndex, enforcedIndex;
if (index <= middleIndex) {
    fixedIndex = middleIndex - 1;
    enforcedIndex = middleIndex + 1;
}
else {
    fixedIndex = middleIndex + 1;
    enforcedIndex = middleIndex - 1;
}
        考虑mirror(镜像)情况,即约束点和固定点关于中间点对称,得到固定点到中间点向量,在固定点上加上这个向量,就能得到约束点修正后的位置。如下图,当选中B时,需要约束的点是A,约束后它的位置应当在A’处,A’与B关于S对称。


1
2
3
4
5
6
7
8
9
10
11
12
if (index <= middleIndex) {
    fixedIndex = middleIndex - 1;
    enforcedIndex = middleIndex + 1;
}
else {
    fixedIndex = middleIndex + 1;
    enforcedIndex = middleIndex - 1;
}
 
Vector3 middle = points[middleIndex];
Vector3 enforcedTangent = middle - points[fixedIndex];
points[enforcedIndex] = middle + enforcedTangent;
        考虑aligned情况,在约束点与固定点中间点共线的前提下,还要保证它与中间点之间的距离不发生改变。因此将约束方向标准化,乘以原来的长度,就是目标约束向量。
1
2
3
4
5
Vector3 enforcedTangent = middle - points[fixedIndex];
if (mode == BezierControlPointMode.Aligned) {
    enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
}
points[enforcedIndex] = middle + enforcedTangent;


       这样一来,当移动控制点时,约束就会被调用,但是由于之前的设计,当移动中间点时,它的前一个点永远是固定的,而后一个点会被约束。如果两个点都随着中间点移动的话,会更加直观一些,因此调整SetControlPoint方法,以达到此目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
public void SetControlPoint (int index, Vector3 point) {
    if (index % 3 == 0) {
        Vector3 delta = point - points[index];
        if (index > 0) {
            points[index - 1] += delta;
        }
        if (index + 1 < points.Length) {
            points[index + 1] += delta;
        }
    }
    points[index] = point;
    EnforceMode(index);
}
       To wrap things up, we should also make sure that the constraints are enforced when we add a curve. We can do this by simply calling EnforceMode at the point where the new curve was added.
       当添加新曲线的时候,也应当施加约束(此时原来曲线的末尾也变成了中间点),做法很简单,只要在AddCurve的最后调用EnforceMode就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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(ref modes, modes.Length + 1);
    modes[modes.Length - 1] = modes[modes.Length - 2];
    EnforceMode(points.Length - 4);
}
       除了强制两条贝塞尔曲线在交点连续的约束外,我们还可以对样条曲线的起点和终点进行约束,使这两个点重合。
      为BezierSpline添加属性loop,当其为true时,我们将首尾两点的mode统一,并且调用SetPosition,它中间已经实现了位置与mode的约束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[SerializeField]
private bool loop;
 
public bool Loop {
    get {
        return loop;
    }
    set {
        loop = value;
        if (value == true) {
            modes[modes.Length - 1] = modes[0];
            SetControlPoint(0, points[0]);
        }
    }
}
       在BezierSplineInspector中使用loop属性,这样在Unity的Inspector里可以勾选,决定曲线是否首尾相连。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override void OnInspectorGUI () {
    spline = target as BezierSpline;
    EditorGUI.BeginChangeCheck();
    bool loop = EditorGUILayout.Toggle("Loop", spline.Loop);
    if (EditorGUI.EndChangeCheck()) {
        Undo.RecordObject(spline, "Toggle Loop");
        EditorUtility.SetDirty(spline);
        spline.Loop = loop;
    }
    if (selectedIndex >= 0 && selectedIndex < spline.ControlPointCount) {
        DrawSelectedPointInspector();
    }
    if (GUILayout.Button("Add Curve")) {
        Undo.RecordObject(spline, "Add Curve");
        spline.AddCurve();
        EditorUtility.SetDirty(spline);
    }
}


          为了正确实现循环曲线,我们还需要对BezierSpline进行修改,在 SetControlPointMode中,如果loop为true,那么当首/尾中的点的mode修改时,另一点的mode也要修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    int modeIndex = (index + 1) / 3;
    modes[modeIndex] = mode;
    if (loop) {
        if (modeIndex == 0) {
            modes[modes.Length - 1] = mode;
        }
        else if (modeIndex == modes.Length - 1) {
            modes[0] = mode;
        }
    }
    EnforceMode(index);
}
        SetControlPoint也需要进行修改。结合loop的值,对传入的index进行多次判断。
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 void SetControlPoint (int index, Vector3 point) {
    if (index % 3 == 0) {
        Vector3 delta = point - points[index];
        if (loop) {
            if (index == 0) {
                points[1] += delta;
                points[points.Length - 2] += delta;
                points[points.Length - 1] = point;
            }
            else if (index == points.Length - 1) {
                points[0] = point;
                points[1] += delta;
                points[index - 1] += delta;
            }
            else {
                points[index - 1] += delta;
                points[index + 1] += delta;
            }
        }
        else {
            if (index > 0) {
                points[index - 1] += delta;
            }
            if (index + 1 < points.Length) {
                points[index + 1] += delta;
            }
        }
    }
    points[index] = point;
    EnforceMode(index);
}
        此时EnforceMode方法也要做出相应的修改,来处理循环曲线的情况,本来对于首尾两个点是不作约束的,但是如果是loop为true的话,首尾点会变成一个中间点。
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
private void EnforceMode (int index) {
    int modeIndex = (index + 1) / 3;
    BezierControlPointMode mode = modes[modeIndex];
    if (mode == BezierControlPointMode.Free || !loop && (modeIndex == 0 || modeIndex == modes.Length - 1)) {
        return;
    }
 
    int middleIndex = modeIndex * 3;
    int fixedIndex, enforcedIndex;
    if (index <= middleIndex) {
        fixedIndex = middleIndex - 1;
        if (fixedIndex < 0) {
            fixedIndex = points.Length - 2;
        }
        enforcedIndex = middleIndex + 1;
        if (enforcedIndex >= points.Length) {
            enforcedIndex = 1;
        }
    }
    else {
        fixedIndex = middleIndex + 1;
        if (fixedIndex >= points.Length) {
            fixedIndex = 1;
        }
        enforcedIndex = middleIndex - 1;
        if (enforcedIndex < 0) {
            enforcedIndex = points.Length - 2;
        }
    }
 
    Vector3 middle = points[middleIndex];
    Vector3 enforcedTangent = middle - points[fixedIndex];
    if (mode == BezierControlPointMode.Aligned) {
        enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
    }
    points[enforcedIndex] = middle + enforcedTangent;
}
        最后,在AddCurve中也要进行loop的情况处理,添加新曲线的时候,首尾也要视情况相连。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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(ref modes, modes.Length + 1);
    modes[modes.Length - 1] = modes[modes.Length - 2];
    EnforceMode(points.Length - 4);
 
    if (loop) {
        points[points.Length - 1] = points[0];
        modes[modes.Length - 1] = modes[0];
        EnforceMode(0);
    }
}


       现在可以通过勾选loop选项,使得我们的样条曲线形成首尾相接的环,但是有个问题,就是我们看不出来初始的起点在哪里了,我们可以在BezierSplineInspector中将起点的图示size变大,便于显示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private Vector3 ShowPoint (int index) {
    Vector3 point = handleTransform.TransformPoint(spline.GetControlPoint(index));
    float size = HandleUtility.GetHandleSize(point);
    if (index == 0) {
        size *= 2f;// 两倍大小
    }
    Handles.color = modeColors[(int)spline.GetControlPointMode(index)];
    if (Handles.Button(point, handleRotation, size * handleSize, size * pickSize, Handles.DotCap)) {
        selectedIndex = index;
        Repaint();
    }
    if (selectedIndex == index) {
        EditorGUI.BeginChangeCheck();
        point = Handles.DoPositionHandle(point, handleRotation);
        if (EditorGUI.EndChangeCheck()) {
            Undo.RecordObject(spline, "Move Point");
            EditorUtility.SetDirty(spline);
            spline.SetControlPoint(index, handleTransform.InverseTransformPoint(point));
        }
    }
    return point;
}


   看了上面的文章 热爱游戏创作的你是不是已经开始热血沸腾了呢?是不是迫不及待的想加入游戏团队成为里面的一员呢?
  福利来啦~赶快加入腾讯GAD交流群,人满封群!每天分享游戏开发内部干货、教学视频、福利活动、和有相同梦想的人在一起,更有腾讯游戏专家手把手教你做游戏!
腾讯GAD游戏程序交流群:484290331

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