在上篇博客中介绍了关于曲线编辑器的制作,下面开始讲如何去编辑,这就需要在曲线上加控制点虽然我们的样条是连续的,但它在曲线段之间会急剧的变化,这些突然变化也导致了点的方向和速度变化,因为两个曲线之间的共享控制点会产生两个不同的速度,每一个曲线都有一个速度。
如果我们想让曲线速度相等,必须确保定义它们的两个控制点——前一条曲线的三分之一和下一条曲线的第二点——在共享点周围镜像,这确保了第一和二阶导数是连续的。
其实,最灵活的方法是决定每条曲线的边界,当然,一旦我们有了这些限制,我们就不能让任何人直接编辑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;
}
效果如下:
代码下载地址:链接:https://pan.baidu.com/s/1KFb1gDfs1yjYyBSkpwSD8w
提取码:k6vh 中的编号04的包。
下篇博客介绍如何使用曲线编辑器。。。。。。