这是如何使贝塞尔曲线的各个锚点连续或不连续的后续问题。请参阅它以获取已接受答案中的相关代码(请注意,我这样做是为了保持这个问题的清洁,因为相关代码很长)。
我正在努力实现以下目标:
使贝塞尔曲线手柄/控制点可选择,以便单个手柄的属性(例如连续性)在选中时显示在检查器窗口中。请注意,我希望在不为手柄/控制点创建游戏对象的情况下完成此操作
保留处理每个点的移动的单一方法,而不是为每个点的移动使用单独的方法。
这是如何使贝塞尔曲线的各个锚点连续或不连续的后续问题。请参阅它以获取已接受答案中的相关代码(请注意,我这样做是为了保持这个问题的清洁,因为相关代码很长)。
我正在努力实现以下目标:
使贝塞尔曲线手柄/控制点可选择,以便单个手柄的属性(例如连续性)在选中时显示在检查器窗口中。请注意,我希望在不为手柄/控制点创建游戏对象的情况下完成此操作
保留处理每个点的移动的单一方法,而不是为每个点的移动使用单独的方法。
我迟到了吗?
使贝塞尔曲线手柄/控制点可选择,以便单个手柄的属性(例如连续性)在选中时显示在检查器窗口中。请注意,我希望在不为手柄/控制点创建游戏对象的情况下完成此操作
我一般喜欢@jour 的解决方案,除了一件事:Handles.Button
您必须单击一个点来选择它,然后单击并拖动以移动控制点。
我提出了一种不同的方法。使用相同的Handles.FreeMoveHandle
,但有一个变量来记住最后点击的句柄的 id,所以我可以识别它。
通常,内置的 Handle 不会为您提供比其设计用途更多的信息。FreeMoveHandle
,例如,返回其翻译的增量,仅此而已。问题是:你想捕捉一个简单的点击,但如果你只是点击而不拖动,返回值是Vector3.zero
,它就像你根本没有点击一样。
好消息:在任何 Handle 的重载中,有一些调用带有一个名为的参数controlID
- 它是每个可交互 GUI 对象的标识符。如果你抑制它,引擎会选择一个,你永远不知道是什么。但是,如果您传递一个 int,则该值将是句柄的 id。但是,如果我通过一个 int 并且它碰巧与我看不到的任何其他 id 冲突?好吧,你可以打电话GUIUtility.GetControlID
来获得一个安全的身份证。
然后,它是直截了当的。如果 Handle 的 id 与EditorGUIUtility.hotControl
(即,被单击或具有键盘焦点的控件)相同,则我保存该点的索引selectedControlPointId
并使用它在 Inspector 中显示自定义属性。
保留处理每个点的移动的单一方法,而不是为每个点的移动使用单独的方法。
嗯...这里成为争议。如果我理解正确,您需要一个代码来绘制节点和切线。问题是:这两件事本质上是不同的。当然,如果你保持简单明了,它们就是场景中的机器人可移动点。但是,当您引入诸如约束(连续性或smooth
)和选择之类的东西时,它们就变成了不同的野兽,具有不同的逻辑。即使您想制作ControlPoint
一个struct
(就像我现在所做的那样)并将其作为一个整体传递,您仍然需要指出您要更新的组件,因此约束将适用于其他组件 - 您将始终需要一个"master" 字段以避免循环更新(您更改tangentBack
,然后tangentFront
更新,触发tangentBack
再次更新等等)。
这就是为什么,即使我以某种方式重新组织了ControlPoint
方法并使其成为 a struct
,我也无法创建一个逻辑来绘制节点和切线。
这里有一些代码。我从上一个问题的答案中的代码重新开始。
控制点.cs
using System;
using UnityEngine;
[Serializable]
public struct ControlPoint
{
public Vector2 position;
public Vector2 tangentBack;
public Vector2 tangentFront;
public bool smooth;
static public ControlPoint MovePosition(ControlPoint pt, Vector2 newPos)
{
var newPt = pt;
newPt.position = newPos;
return newPt;
}
static public ControlPoint MoveTangentBack(ControlPoint pt, Vector2 newTanBack)
{
var newPt = pt;
newPt.tangentBack = newTanBack;
if (pt.smooth) newPt.tangentFront = pt.tangentFront.magnitude * -newTanBack.normalized;
return newPt;
}
static public ControlPoint MoveTangentFront(ControlPoint pt, Vector2 newTanFront)
{
var newPt = pt;
newPt.tangentFront = newTanFront;
if (pt.smooth) newPt.tangentBack = pt.tangentBack.magnitude * -newTanFront.normalized;
return newPt;
}
static public ControlPoint WithSmooth(ControlPoint pt, bool smooth)
{
var newPt = pt;
if (smooth != pt.smooth) newPt.tangentBack = -pt.tangentFront;
return newPt;
}
public ControlPoint(Vector2 position, Vector2 tanBack, Vector2 tanFront, bool smooth = false)
{
this.position = position;
this.tangentBack = tanBack;
this.tangentFront = tanFront;
this.smooth = smooth;
}
}
我删除ControlPointDrawer
了 ,因此您添加的其他属性不会隐藏在检查器中。
路径.cs
using System;
using UnityEngine;
using System.Collections.Generic;
[Serializable]
public class Path
{
[SerializeField] List<ControlPoint> _points;
[SerializeField] bool _loop = false;
public Path(Vector2 position)
{
_points = new List<ControlPoint>
{
new ControlPoint(position, -Vector2.one, Vector2.one),
new ControlPoint(position + Vector2.right, -Vector2.one, Vector2.one)
};
}
public bool loop { get { return _loop; } set { _loop = value; } }
public ControlPoint this[int i]
{
get { return _points[(_loop && i == _points.Count) ? 0 : i]; }
set { _points[(_loop && i == _points.Count) ? 0 : i] = value; }
}
public int NumPoints { get { return _points.Count; } }
public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }
public ControlPoint InsertPoint(int i, Vector2 position)
{
_points.Insert(i, new ControlPoint(position, -Vector2.one, Vector2.one));
return this[i];
}
public ControlPoint RemovePoint(int i)
{
var item = this[i];
_points.RemoveAt(i);
return item;
}
public Vector2[] GetBezierPointsInSegment(int i)
{
var pointBack = this[i];
var pointFront = this[i + 1];
return new Vector2[4]
{
pointBack.position,
pointBack.position + pointBack.tangentFront,
pointFront.position + pointFront.tangentBack,
pointFront.position
};
}
public ControlPoint MovePoint(int i, Vector2 position)
{
this[i] = ControlPoint.MovePosition(this[i], position);
return this[i];
}
public ControlPoint MoveTangentBack(int i, Vector2 position)
{
this[i] = ControlPoint.MoveTangentBack(this[i], position);
return this[i];
}
public ControlPoint MoveTangentFront(int i, Vector2 position)
{
this[i] = ControlPoint.MoveTangentFront(this[i], position);
return this[i];
}
}
PathCreator.cs是一样的
PathCreatorEditor.cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
PathCreator creator;
Path path;
SerializedProperty property;
static int selectedControlPointId = -1;
public override void OnInspectorGUI()
{
serializedObject.Update();
var loopProp = property.FindPropertyRelative("_loop");
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(loopProp);
var ptsProp = property.FindPropertyRelative("_points");
var msg = "Total points in path: " + ptsProp.arraySize + "\n";
if (selectedControlPointId >= 0 && ptsProp.arraySize > 0)
{
EditorGUILayout.HelpBox(msg + "Selected control point: " + selectedControlPointId, MessageType.Info);
EditorGUILayout.Separator();
EditorGUILayout.PropertyField(ptsProp.GetArrayElementAtIndex(selectedControlPointId), true);
}
else
{
EditorGUILayout.HelpBox(msg + "No control points selected", MessageType.Info);
}
if (EditorGUI.EndChangeCheck()) serializedObject.ApplyModifiedProperties();
}
void OnSceneGUI()
{
Input();
Draw();
}
void Input()
{
Event guiEvent = Event.current;
Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
mousePos = creator.transform.InverseTransformPoint(mousePos);
if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
{
Undo.RecordObject(creator, "Insert point");
path.InsertPoint(path.NumPoints, mousePos);
}
else if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.control)
{
for (int i = 0; i < path.NumPoints; i++)
{
if (Vector2.Distance(mousePos, path[i].position) <= .25f)
{
Undo.RecordObject(creator, "Remove point");
path.RemovePoint(i);
break;
}
}
}
}
void Draw()
{
Handles.matrix = creator.transform.localToWorldMatrix;
var rot = Quaternion.Inverse(creator.transform.rotation) * Tools.handleRotation;
var snap = Vector2.zero;
Handles.CapFunction cap = Handles.DotHandleCap;
for (int i = 0; i < path.NumPoints; i++)
{
float size;
var pos = path[i].position;
size = HandleUtility.GetHandleSize(pos) * .05f;
Handles.Label(pos, i.ToString());
Handles.color = i == selectedControlPointId ? Handles.selectedColor : Color.red;
int ctrlId = GUIUtility.GetControlID(FocusType.Passive);
Vector2 newPos = Handles.FreeMoveHandle(ctrlId, pos, rot, size, snap, cap);
if (ctrlId == EditorGUIUtility.hotControl) selectedControlPointId = i;
if (pos != newPos)
{
Undo.RecordObject(creator, "Move point position");
path.MovePoint(i, newPos);
}
pos = newPos;
Handles.color = Color.black;
if (path.loop || i != 0)
{
var tanBack = pos + path[i].tangentBack;
Handles.DrawLine(pos, tanBack);
size = HandleUtility.GetHandleSize(tanBack) * .03f;
Vector2 newTanBack = Handles.FreeMoveHandle(tanBack, rot, size, snap, cap);
if (tanBack != newTanBack)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentBack(i, newTanBack - pos);
}
}
if (path.loop || i != path.NumPoints - 1)
{
var tanFront = pos + path[i].tangentFront;
Handles.DrawLine(pos, tanFront);
size = HandleUtility.GetHandleSize(tanFront) * .03f;
Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
if (tanFront != newTanFront)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentFront(i, newTanFront - pos);
}
}
}
Repaint();
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected | GizmoType.Pickable)]
static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
{
Gizmos.matrix = creator.transform.localToWorldMatrix;
var path = creator.path;
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetBezierPointsInSegment(i);
var pts = Handles.MakeBezierPoints(points[0], points[3], points[1], points[2], 30);
Gizmos.color = Color.green;
for (int j = 0; j < pts.Length - 1; j++)
{
Gizmos.DrawLine(pts[j], pts[j + 1]);
}
}
}
void OnEnable()
{
creator = (PathCreator)target;
path = creator.path ?? creator.CreatePath();
property = serializedObject.FindProperty("path");
}
}
注意:我将贝塞尔线的绘图从 移动OnSceneGui
到DrawGizmo
,因此即使对象没有被选中,绿线也会可见,并且可以在场景编辑器中拾取它,以便选中它。
最后,我建议对该脚本进行一些进一步的开发。使多点选择成为可能并不难。也许使默认手柄(如位置和旋转)可单独应用于点。或者将创建和删除点的方法更改为直观的移动方式,例如双击或拖动路径线。甚至是用于智能点操作的自定义工具栏,如对齐、分布、雕刻......不同的约束,如平滑、对称、尖点或直线......
不完全确定我是否理解了这个问题,但是
使贝塞尔曲线手柄/控制点可选择,以便单个手柄的属性(例如连续性)在选中时显示在检查器窗口中。请注意,我希望在不为手柄/控制点创建游戏对象的情况下完成此操作
您需要使用 OnSceneGUI 选项才能选择手柄,并且每次选择新点时只需存储其值。
private void OnSceneGUI ()
{
spline = target as Path;
handleTransform = spline.transform;
handleRotation = Tools.pivotRotation == PivotRotation.Local
? handleTransform.rotation : Quaternion.identity;
Vector3 p0 = ShowPoint (0);
Color gg = Color.gray;
gg.a = 0.25f;
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 = gg;
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.Points[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 (handleTransform.InverseTransformPoint (point), index);
}
}
return point;
}
并在检查员 GUI 上显示要点
private void DrawSelectedPointInspector ()
{
GUILayout.Label ("Selected Point");
EditorGUI.BeginChangeCheck ();
Vector3 point = EditorGUILayout.Vector3Field ("Position", spline.Points[selectedIndex]);
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Move Point");
EditorUtility.SetDirty (spline);
spline.SetControlPoint (point, selectedIndex);
}
EditorGUI.BeginChangeCheck ();
BezierControlPointModes mode = (BezierControlPointModes) EditorGUILayout.EnumPopup ("Mode", spline.GetControlPointMode (selectedIndex));
if (EditorGUI.EndChangeCheck ())
{
Undo.RecordObject (spline, "Change Point Mode");
spline.SetControlPointMode (selectedIndex, mode);
EditorUtility.SetDirty (spline);
}
}
并在 InspectorGUI 上调用它
if (selectedIndex >= 0 && selectedIndex < spline.points.Count)
{
DrawSelectedPointInspector ();
}
保留处理每个点的移动的单一方法,而不是为每个点的移动使用单独的方法。为此,您只需要保留