上篇文章给大家介绍了关于曲线编辑器的制作,下面开始讲如何去编辑,这就需要在曲线上加控制点虽然我们的样条是连续的,但它在曲线段之间会急剧的变化,这些突然变化也导致了点的方向和速度变化,因为两个曲线之间的共享控制点会产生两个不同的速度,每一个曲线都有一个速度。 如果我们想让曲线速度相等,必须确保定义它们的两个控制点——前一条曲线的三分之一和下一条曲线的第二点——在共享点周围镜像,这确保了第一和二阶导数是连续的。
其实,最灵活的方法是决定每条曲线的边界,当然,一旦我们有了这些限制,我们就不能让任何人直接编辑BezierSpline的点,因此,让我们将数组私有并提供对它的间接访问,确保让Unity知道我们仍然想要序列化我们的点,否则它们就不会被保存。代码如下所示:
- [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必须使用新的方法和属性,而不是直接访问点数组。
- 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;
- }
当然 ,我们也不再希望允许直接访问检查器中的数组,因此取消对DrawDefaultInspector的调用,为了仍然允许通过输入进行更改,让我们为选定的点显示一个向量场。
- 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);
- }
- }
另外,当我们在场景视图中选择一个点时,检查器不会刷新自己。我们可以通过调用spline的SetDirty来解决这个问题,但这并不正确,因为样条没有改变。幸运的是,我们可以发出重新绘制的请求。
- 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;
- }

让我们定义一个枚举类型来描述我们的三种模式。创建一个新脚本,删除默认代码,并使用三个选项定义enum。
- public enum BezierControlPointMode {
- Free,
- Aligned,
- Mirrored
- }
您需要重置您的样条或创建一个新的样条,以确保您有合适大小的数组。
- <span style="white-space:pre"> </span>[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
- };
- }
当我们在曲线之间存储模式时,如果我们能得到和设置每个控制点的模式是很方便的。因此,我们需要将点索引转换为模式索引,因为在现实点
共享模式。作为一个例子,点索引序列0、1、2、3、4、5、6对应于模式索引序列0、0、1、1、1、2、2。所以我们需要加1然后除以3。
- public BezierControlPointMode GetControlPointMode (int index) {
- return modes[(index 1) / 3];
- }
-
- public void SetControlPointMode (int index, BezierControlPointMode mode) {
- modes[(index 1) / 3] = mode;
- }
现在,BezierSplineInspector可以允许我们改变所选点的模式,你会注意到,改变一个点的模式似乎也会改变与之相关的点的模式。
- 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);
- }
- }

如果我们在场景视图中得到一些关于节点类型的可视化反馈,那将是很有用的,我们可以很容易地把这些点涂上颜色,我将用白色表示自由,黄色表示对齐。
- 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添加一个新的方法,当一个点移动或模式发生改变时调用它,它需要一个点索引,并从检索相关模式开始。
- 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;
- }
我们应该检查是否真的没有强制执行任何东西。当模式被设置为自由时,或者当我们在曲线的端点时。在这些情况下,我们可以在不做任何事情的情况下返回。
- 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;
- }
- }
现在 我们应该调整哪一点?当我们改变一个点的模式时,它要么是曲线上的一点,要么是它的一个相邻的点。当我们选择中间点时,我们可以保持之前的点固定并在对边的点上执行约束。如果我们选择了另一个点,我们应该保持一个固定和调整它的反向。这样我们选择的点总是保持在它所在的位置,我们来定义这些点的指数。
- if (mode == BezierControlPointMode.Free || modeIndex == 0 || modeIndex == modes.Length - 1) {
- return;
- }
-
- int middleIndex = modeIndex * 3;
- int fixedIndex, enforcedIndex;
- if (index <= middleIndex) {
- fixedIndex = middleIndex - 1;
- enforcedIndex = middleIndex 1;
- }
- else {
- fixedIndex = middleIndex 1;
- enforcedIndex = middleIndex - 1;
- }
让我们首先考虑镜像的情况,为了在中间点镜像,我们必须从中间的向量取到固定的点——它是(固定的-中)-并把它倒过来,这是切线,
把它加到中间,得到了我们的执行点。
- 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;
对齐模式,我们还必须确保新切线的长度与以前的相同,我们把它标准化,然后乘以中间的和以前的控制点之间的距离。
- Vector3 enforcedTangent = middle - points[fixedIndex];
- if (mode == BezierControlPointMode.Aligned) {
- enforcedTangent = enforcedTangent.normalized * Vector3.Distance(middle, points[enforcedIndex]);
- }
- points[enforcedIndex] = middle enforcedTangent;

终于看到效果了,从现在开始,每当你移动一个点或改变点的模式时,约束就会被执行,但当移动中间点时,前一点总是固定不变,下一个点总是被强制执行,这可能很好,但如果两个点都和中间的点一起移动的话,这是很直观的,我们来调整SetControlPoint把它们移动到一起。
- 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);
- }
还应该确保在添加曲线时,约束被执行,可以通过在添加新曲线的地方调用强制执行程序来实现这一点。
- 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。当它设置为true时,我们确保端点匹配的模式和我们调用SetPosition,相信它会处理位置和模式约束。
- [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:
- 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需要确保在循环的情况下,第一个和最后一个模式保持相等。
- public void SetControlPointMode (int index, BezierControlPointMode mode) {
- 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需要不同的边界情况。
- 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函数中,强制执行的只能是在最后的时候,它还必须检查固定的或强制的点:
- 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;
- }
最后,我们还需要在向样条添加曲线时考虑循环:
- 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);
- }
- }

我们有循环了,但我们不能再看到样条从哪里开始,这很不方便,我们可以通过让BezierSplineInspector总是将第一点的点的大小增加一倍来说明这一点。
注意,如果在一个循环中,最后一点会被绘制在上面,所以如果你点击了大圆点的中间,你会选择最后一点,而如果你从中间点击了,你会得到第一个点。
- 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;
- }
效果如下:

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