Unity中的曲线绘制(四)——约束控制点
发表于2016-10-31
导语
BezierSplineInspector中的获取控制点相关代码要进行调整。
既然将控制点封装了,我们就不能在默认的Inspector中修改它们的位置了,但是我们可以自定义一个绘制方法,用于输入数值修改选中的控制点的位置。
但是,当我们选中一个点,但是不去拖动它的话,Inspector中并不会刷新,应当在选中点时重新绘制Inspector。
为每个控制点提供简便的设置/获取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两条贝塞尔曲线。
为BezierSplineInspector添加功能,当选中控制点时,可以在Inspector中修改该点的mode以及与该点关联的mode。
判别是否需要约束,当该点的mode为Free时,或者处理对象是曲线的第一个点或者最后一个点,那么不用做任何处理,直接返回
当目标点是两条贝塞尔曲线的交点,及其相邻点时,我们要对其进行约束。选中中间点时,保持它上一个点固定不动,对下一个点进行处理。而选中中间点的相邻点时,对其对面的点进行约束,也就是说,选中的那个点是永远固定的。
考虑mirror(镜像)情况,即约束点和固定点关于中间点对称,得到固定点到中间点向量,在固定点上加上这个向量,就能得到约束点修正后的位置。如下图,当选中B时,需要约束的点是A,约束后它的位置应当在A’处,A’与B关于S对称。
考虑aligned情况,在约束点与固定点中间点共线的前提下,还要保证它与中间点之间的距离不发生改变。因此将约束方向标准化,乘以原来的长度,就是目标约束向量。
在BezierSplineInspector中使用loop属性,这样在Unity的Inspector里可以勾选,决定曲线是否首尾相连。
SetControlPoint也需要进行修改。结合loop的值,对传入的index进行多次判断
此时EnforceMode方法也要做出相应的修改,来处理循环曲线的情况,本来对于首尾两个点是不作约束的,但是如果是loop为true的话,首尾点会变成一个中间点。
最后,在AddCurve中也要进行loop的情况处理,添加新曲线的时候,首尾也要视情况相连。
BezierSplineInspector中的获取控制点相关代码要进行调整。
既然将控制点封装了,我们就不能在默认的Inspector中修改它们的位置了,但是我们可以自定义一个绘制方法,用于输入数值修改选中的控制点的位置。
但是,当我们选中一个点,但是不去拖动它的话,Inspector中并不会刷新,应当在选中点时重新绘制Inspector。
为每个控制点提供简便的设置/获取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两条贝塞尔曲线。
为BezierSplineInspector添加功能,当选中控制点时,可以在Inspector中修改该点的mode以及与该点关联的mode。
判别是否需要约束,当该点的mode为Free时,或者处理对象是曲线的第一个点或者最后一个点,那么不用做任何处理,直接返回
当目标点是两条贝塞尔曲线的交点,及其相邻点时,我们要对其进行约束。选中中间点时,保持它上一个点固定不动,对下一个点进行处理。而选中中间点的相邻点时,对其对面的点进行约束,也就是说,选中的那个点是永远固定的。
考虑mirror(镜像)情况,即约束点和固定点关于中间点对称,得到固定点到中间点向量,在固定点上加上这个向量,就能得到约束点修正后的位置。如下图,当选中B时,需要约束的点是A,约束后它的位置应当在A’处,A’与B关于S对称。
考虑aligned情况,在约束点与固定点中间点共线的前提下,还要保证它与中间点之间的距离不发生改变。因此将约束方向标准化,乘以原来的长度,就是目标约束向量。
在BezierSplineInspector中使用loop属性,这样在Unity的Inspector里可以勾选,决定曲线是否首尾相连。
SetControlPoint也需要进行修改。结合loop的值,对传入的index进行多次判断。
此时EnforceMode方法也要做出相应的修改,来处理循环曲线的情况,本来对于首尾两个点是不作约束的,但是如果是loop为true的话,首尾点会变成一个中间点。
最后,在AddCurve中也要进行loop的情况处理,添加新曲线的时候,首尾也要视情况相连。
对样条曲线做出一些条件约束,两条贝塞尔曲线的衔接点处切线要连续,移动衔接点时相邻点也要同步移动。还可以设置条件属性来实现曲线成环,首尾相连。
约束控制点
尽管我们的样条曲线是连续的,但是在衔接点(第一条曲线的最后一个点/第二条曲线的第一个点),方向会发生突变。这是因为在这个点,两条曲线有不同方向的速度,所以会出现转折。
如果我们希望在衔接点## 约束控制点
尽管我们的样条曲线是连续的,但是在共享的控制点(第一条曲线的最后一个点/第二条曲线的第一个点),方向会发生突变。这是因为在这个点,两条曲线有不同方向的速度,所以会出现转折。
如果我们希望在衔接点两条曲线的速度一致,那么该点的前一个点和后一个点(第一条曲线的倒数第二个点和第二条曲线的第二个点)应当和衔接点在同一条线上。如下图,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; } |
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; |
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); } |
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 }; } |
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; } |
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; } |
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; } |
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; |
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]); } } } |
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); } |
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); } |
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; } |
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; } |
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; } |
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); } |
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 }; } |
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; } |
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; } |
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; } |
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; |
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]); } } } |
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); } |
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); } |
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; } |
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; } |