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

来源:互联网 发布:t mac麦迪2代简版测评 编辑:程序博客网 时间:2024/06/03 22:56
在上篇博客中介绍了关于曲线编辑器的制作,下面开始讲如何去编辑,这就需要在曲线上加控制点虽然我们的样条是连续的,但它在曲线段之间会急剧的变化,这些突然变化也导致了点的方向和速度变化,因为两个曲线之间的共享控制点会产生两个不同的速度,每一个曲线都有一个速度。

如果我们想让曲线速度相等,必须确保定义它们的两个控制点——前一条曲线的三分之一和下一条曲线的第二点——在共享点周围镜像,这确保了第一和二阶导数是连续的。

其实,最灵活的方法是决定每条曲线的边界,当然,一旦我们有了这些限制,我们就不能让任何人直接编辑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}

现在我们可以将这些模式添加到BezierSpline中,我们只需要在曲线之间存储模式,让我们把它们放到一个长度等于曲线数+ 1的数组中,

您需要重置您的样条或创建一个新的样条,以确保您有合适大小的数组。

[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的包。

下篇博客介绍如何使用曲线编辑器。。。。。。


原创粉丝点击