如何使贝塞尔曲线的单个锚点连续或不连续
How to make individual anchor points of bezier continuous or non-continuous
我正在使用以下代码创建贝塞尔曲线。通过在场景视图中按住 Shift 单击,可以扩展曲线以连接多条贝塞尔曲线。我的代码具有使整个曲线连续或不连续的功能。我意识到我需要让个别点(特别是锚点)具有此功能。
我认为最理想的方法是为具有此功能的点创建一个新的 class(使点连续或不连续),因为这可用于添加其他可能的属性具体到点。怎么做到的?
路径
[System.Serializable]
public class Path {
[SerializeField, HideInInspector]
List<Vector2> points;
[SerializeField, HideInInspector]
public bool isContinuous;
public Path(Vector2 centre)
{
points = new List<Vector2>
{
centre+Vector2.left,
centre+(Vector2.left+Vector2.up)*.5f,
centre + (Vector2.right+Vector2.down)*.5f,
centre + Vector2.right
};
}
public Vector2 this[int i]
{
get
{
return points[i];
}
}
public int NumPoints
{
get
{
return points.Count;
}
}
public int NumSegments
{
get
{
return (points.Count - 4) / 3 + 1;
}
}
public void AddSegment(Vector2 anchorPos)
{
points.Add(points[points.Count - 1] * 2 - points[points.Count - 2]);
points.Add((points[points.Count - 1] + anchorPos) * .5f);
points.Add(anchorPos);
}
public Vector2[] GetPointsInSegment(int i)
{
return new Vector2[] { points[i * 3], points[i * 3 + 1], points[i * 3 + 2], points[i * 3 + 3] };
}
public void MovePoint(int i, Vector2 pos)
{
if (isContinuous)
{
Vector2 deltaMove = pos - points[i];
points[i] = pos;
if (i % 3 == 0)
{
if (i + 1 < points.Count)
{
points[i + 1] += deltaMove;
}
if (i - 1 >= 0)
{
points[i - 1] += deltaMove;
}
}
else
{
bool nextPointIsAnchor = (i + 1) % 3 == 0;
int correspondingControlIndex = (nextPointIsAnchor) ? i + 2 : i - 2;
int anchorIndex = (nextPointIsAnchor) ? i + 1 : i - 1;
if (correspondingControlIndex >= 0 && correspondingControlIndex < points.Count)
{
float dst = (points[anchorIndex] - points[correspondingControlIndex]).magnitude;
Vector2 dir = (points[anchorIndex] - pos).normalized;
points[correspondingControlIndex] = points[anchorIndex] + dir * dst;
}
}
}
}
else {
points[i] = pos;
}
}
路径创建器
public class PathCreator : MonoBehaviour {
[HideInInspector]
public Path path;
public void CreatePath()
{
path = new Path(transform.position);
}
}
路径编辑器
[CustomEditor(typeof(PathCreator))]
public class PathEditor : Editor {
PathCreator creator;
Path path;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUI.BeginChangeCheck();
bool continuousControlPoints = GUILayout.Toggle(path.isContinuous, "Set Continuous Control Points");
if (continuousControlPoints != path.isContinuous)
{
Undo.RecordObject(creator, "Toggle set continuous controls");
path.isContinuous = continuousControlPoints;
}
if (EditorGUI.EndChangeCheck())
{
SceneView.RepaintAll();
}
}
void OnSceneGUI()
{
Input();
Draw();
}
void Input()
{
Event guiEvent = Event.current;
Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
{
Undo.RecordObject(creator, "Add segment");
path.AddSegment(mousePos);
}
}
void Draw()
{
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetPointsInSegment(i);
Handles.color = Color.black;
Handles.DrawLine(points[1], points[0]);
Handles.DrawLine(points[2], points[3]);
Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
}
Handles.color = Color.red;
for (int i = 0; i < path.NumPoints; i++)
{
Vector2 newPos = Handles.FreeMoveHandle(path[i], Quaternion.identity, .1f, Vector2.zero, Handles.CylinderHandleCap);
if (path[i] != newPos)
{
Undo.RecordObject(creator, "Move point");
path.MovePoint(i, newPos);
}
}
}
void OnEnable()
{
creator = (PathCreator)target;
if (creator.path == null)
{
creator.CreatePath();
}
path = creator.path;
}
}
我觉得你的想法很好:你可以写两个classes,命名为ControlPoint
和HandlePoint
(使它们可序列化)。
ControlPoint
可能代表每条曲线的 p0
和 p3
- 路径确实经过的点。对于连续性,您必须断言一个段的p3
等于下一段的p0
。
HandlePoint
可以表示每条曲线的 p1
和 p2
- 曲线的切点并提供方向和倾斜度。对于 平滑度 ,您必须断言一个段的 (p3 - p2).normalized
等于下一段的 (p1 - p0).normalized
。 (如果你想要 对称平滑度 ,一个的 p3 - p2
必须等于另一个的 p1 - p0
。)
提示#1:在分配或比较每个段的点时始终考虑矩阵变换。我建议你在执行操作之前将任何点转换为全局 space。
提示 #2:考虑在段内的点之间应用约束,因此当您在曲线的 p0
或 p3
周围移动时,p1
或 p2
分别相应地移动相同的量(就像任何图形编辑器软件在贝塞尔曲线上所做的一样)。
编辑 -> 提供的代码
我做了一个想法的示例实现。实际上,在开始编码之后,我意识到只需一个 class ControlPoint
(而不是两个)就可以完成这项工作。 A ControlPoint
有 2 条切线。所需的行为由字段 smooth
控制,可以为每个点设置。
ControlPoint.cs
using System;
using UnityEngine;
[Serializable]
public class ControlPoint
{
[SerializeField] Vector2 _position;
[SerializeField] bool _smooth;
[SerializeField] Vector2 _tangentBack;
[SerializeField] Vector2 _tangentFront;
public Vector2 position
{
get { return _position; }
set { _position = value; }
}
public bool smooth
{
get { return _smooth; }
set { if (_smooth = value) _tangentBack = -_tangentFront; }
}
public Vector2 tangentBack
{
get { return _tangentBack; }
set
{
_tangentBack = value;
if (_smooth) _tangentFront = _tangentFront.magnitude * -value.normalized;
}
}
public Vector2 tangentFront
{
get { return _tangentFront; }
set
{
_tangentFront = value;
if (_smooth) _tangentBack = _tangentBack.magnitude * -value.normalized;
}
}
public ControlPoint(Vector2 position, bool smooth = true)
{
this._position = position;
this._smooth = smooth;
this._tangentBack = -Vector2.one;
this._tangentFront = Vector2.one;
}
}
我还为 ControlPoint
class 编写了一个自定义 PropertyDrawer
,因此它可以更好地显示在检查器上。这只是一个天真的实现。你可以大大改进它。
ControlPointDrawer.cs
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(ControlPoint))]
public class ControlPointDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0; //-= 1;
var propPos = new Rect(position.x, position.y, position.x + 18, position.height);
var prop = property.FindPropertyRelative("_smooth");
EditorGUI.PropertyField(propPos, prop, GUIContent.none);
propPos = new Rect(position.x + 20, position.y, position.width - 20, position.height);
prop = property.FindPropertyRelative("_position");
EditorGUI.PropertyField(propPos, prop, GUIContent.none);
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
}
我遵循了与您的解决方案相同的架构,但进行了必要的调整以适应 ControlPoint
class 和其他 fixes/changes。例如,我将所有点值存储在本地坐标中,因此组件或父级上的变换反映在曲线中。
Path.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),
new ControlPoint(position + Vector2.right)
};
}
public bool loop { get { return _loop; } set { _loop = value; } }
public ControlPoint this[int i] { get { return _points[(_loop && i == _points.Count) ? 0 : i]; } }
public int NumPoints { get { return _points.Count; } }
public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }
public ControlPoint InsertPoint(int i, Vector2 position, bool smooth)
{
_points.Insert(i, new ControlPoint(position, smooth));
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].position = position;
return this[i];
}
public ControlPoint MoveTangentBack(int i, Vector2 position)
{
this[i].tangentBack = position;
return this[i];
}
public ControlPoint MoveTangentFront(int i, Vector2 position)
{
this[i].tangentFront = position;
return this[i];
}
}
PathEditor
几乎是一回事。
PathCreator.cs
using UnityEngine;
public class PathCreator : MonoBehaviour
{
public Path path;
public Path CreatePath()
{
return path = new Path(Vector2.zero);
}
void Reset()
{
CreatePath();
}
}
最后,所有的魔法都发生在 PathCreatorEditor
。这里有两条评论:
1) 我将线条的绘制移至自定义 DrawGizmo
静态函数,因此即使对象不是 Active
(即在检查器中显示),您也可以绘制线条如果你愿意,甚至可以让它变得可挑选。我不知道你是否想要这种行为,但你可以轻松恢复;
2) 注意 class 上方的 Handles.matrix = creator.transform.localToWorldMatrix
行。它会自动将点的比例和旋转转换为世界坐标。那边也有PivotRotation
的细节
PathCreatorEditor.cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
PathCreator creator;
Path path;
SerializedProperty property;
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(property, true);
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, false);
}
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 = Tools.pivotRotation == PivotRotation.Local ? creator.transform.rotation : Quaternion.identity;
var snap = Vector2.zero;
Handles.CapFunction cap = Handles.CylinderHandleCap;
for (int i = 0; i < path.NumPoints; i++)
{
var pos = path[i].position;
var size = .1f;
Handles.color = Color.red;
Vector2 newPos = Handles.FreeMoveHandle(pos, rot, size, snap, cap);
if (pos != newPos)
{
Undo.RecordObject(creator, "Move point position");
path.MovePoint(i, newPos);
}
pos = newPos;
if (path.loop || i != 0)
{
var tanBack = pos + path[i].tangentBack;
Handles.color = Color.black;
Handles.DrawLine(pos, tanBack);
Handles.color = Color.red;
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.color = Color.black;
Handles.DrawLine(pos, tanFront);
Handles.color = Color.red;
Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
if (tanFront != newTanFront)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentFront(i, newTanFront - pos);
}
}
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
{
Handles.matrix = creator.transform.localToWorldMatrix;
var path = creator.path;
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetBezierPointsInSegment(i);
Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
}
}
void OnEnable()
{
creator = (PathCreator)target;
path = creator.path ?? creator.CreatePath();
property = serializedObject.FindProperty("path");
}
}
此外,我添加了一个 loop
字段以防您希望曲线闭合,并且我添加了一个简单的功能来通过 Ctrl+click
在场景中删除点。
总而言之,这只是基本的东西,但您可以根据需要进行高级操作。此外,您可以将 ControlPoint class 与其他组件重用,例如 Catmull-Rom 样条、几何形状、其他参数函数...
你的 post 中的基本问题是:'Is it a good idea to a have a separate Class for the points of a bezier curve?'
由于曲线将由这样的点组成,而且这些点不仅仅是两个坐标 imo 这肯定是个好主意。
但是,像往常一样,在进行 class 设计时,让我们收集一些 用例 ,即点将用于的事情或我们期望做的事情一点..:[=17=]
- 可以在曲线上添加或删除点
- 可以移动一个点
- 它的控制点可以移动
除了单纯的位置,一个点,即'anchor point'应该有更多的属性,abilities/methods..:[=17=]
有控制点;这些与要点的关系有时并不完全相同。查看 Unity 文档,我们看到 Handles.DrawLine
查看两个点及其 'inner' 控制点。来自 GDI+ GraphicsPath
我看到一系列点,在 1 个锚点和 2 个控制点之间交替。 Imo,这为将两个控制点视为锚点的属性提供了更有力的理由。因为两者都必须是可移动的,所以它们可能有一个共同的祖先或连接到 movecontroller
class;但我相信你最了解如何在 Unity 中做到这一点..
真正开始的 属性 问题类似于 bool IsContinuous
。当true
我们需要耦合
- 移动一个控制点以'the opposite'方式移动另一个控制点。
- 将锚点移动到平行移动两个控制点
- 也许 属性
bool IsLocked
以防止移动它
- 也许 属性
bool IsProtected
以防止在 reducing/simplifying 曲线时删除它。 (这对于构造曲线几乎不需要,但对于徒手绘制或用鼠标追踪的曲线非常需要)
- 也许一个属性知道可以一起编辑的一组点中的点。
- 也许是一个通用标记。
- 可能是文本注释
- 可能是表示曲线中 break/split 的类型指示符。
- 可能是增加或减少平滑度与尖锐度的方法。
一些用例显然主要涉及曲线,但其他用例不涉及;有些对两者都有用。
所以,显然我们有很多充分的理由来创建一个聪明的 ÀnchPoint` class..
((我有点忙,但仍计划为 GraphicsPath 贝塞尔曲线编写我自己的编辑器。如果发生这种情况,我会用我学到的东西更新 post,包括class 我想出的设计..))
我正在使用以下代码创建贝塞尔曲线。通过在场景视图中按住 Shift 单击,可以扩展曲线以连接多条贝塞尔曲线。我的代码具有使整个曲线连续或不连续的功能。我意识到我需要让个别点(特别是锚点)具有此功能。
我认为最理想的方法是为具有此功能的点创建一个新的 class(使点连续或不连续),因为这可用于添加其他可能的属性具体到点。怎么做到的?
路径
[System.Serializable]
public class Path {
[SerializeField, HideInInspector]
List<Vector2> points;
[SerializeField, HideInInspector]
public bool isContinuous;
public Path(Vector2 centre)
{
points = new List<Vector2>
{
centre+Vector2.left,
centre+(Vector2.left+Vector2.up)*.5f,
centre + (Vector2.right+Vector2.down)*.5f,
centre + Vector2.right
};
}
public Vector2 this[int i]
{
get
{
return points[i];
}
}
public int NumPoints
{
get
{
return points.Count;
}
}
public int NumSegments
{
get
{
return (points.Count - 4) / 3 + 1;
}
}
public void AddSegment(Vector2 anchorPos)
{
points.Add(points[points.Count - 1] * 2 - points[points.Count - 2]);
points.Add((points[points.Count - 1] + anchorPos) * .5f);
points.Add(anchorPos);
}
public Vector2[] GetPointsInSegment(int i)
{
return new Vector2[] { points[i * 3], points[i * 3 + 1], points[i * 3 + 2], points[i * 3 + 3] };
}
public void MovePoint(int i, Vector2 pos)
{
if (isContinuous)
{
Vector2 deltaMove = pos - points[i];
points[i] = pos;
if (i % 3 == 0)
{
if (i + 1 < points.Count)
{
points[i + 1] += deltaMove;
}
if (i - 1 >= 0)
{
points[i - 1] += deltaMove;
}
}
else
{
bool nextPointIsAnchor = (i + 1) % 3 == 0;
int correspondingControlIndex = (nextPointIsAnchor) ? i + 2 : i - 2;
int anchorIndex = (nextPointIsAnchor) ? i + 1 : i - 1;
if (correspondingControlIndex >= 0 && correspondingControlIndex < points.Count)
{
float dst = (points[anchorIndex] - points[correspondingControlIndex]).magnitude;
Vector2 dir = (points[anchorIndex] - pos).normalized;
points[correspondingControlIndex] = points[anchorIndex] + dir * dst;
}
}
}
}
else {
points[i] = pos;
}
}
路径创建器
public class PathCreator : MonoBehaviour {
[HideInInspector]
public Path path;
public void CreatePath()
{
path = new Path(transform.position);
}
}
路径编辑器
[CustomEditor(typeof(PathCreator))]
public class PathEditor : Editor {
PathCreator creator;
Path path;
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUI.BeginChangeCheck();
bool continuousControlPoints = GUILayout.Toggle(path.isContinuous, "Set Continuous Control Points");
if (continuousControlPoints != path.isContinuous)
{
Undo.RecordObject(creator, "Toggle set continuous controls");
path.isContinuous = continuousControlPoints;
}
if (EditorGUI.EndChangeCheck())
{
SceneView.RepaintAll();
}
}
void OnSceneGUI()
{
Input();
Draw();
}
void Input()
{
Event guiEvent = Event.current;
Vector2 mousePos = HandleUtility.GUIPointToWorldRay(guiEvent.mousePosition).origin;
if (guiEvent.type == EventType.MouseDown && guiEvent.button == 0 && guiEvent.shift)
{
Undo.RecordObject(creator, "Add segment");
path.AddSegment(mousePos);
}
}
void Draw()
{
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetPointsInSegment(i);
Handles.color = Color.black;
Handles.DrawLine(points[1], points[0]);
Handles.DrawLine(points[2], points[3]);
Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
}
Handles.color = Color.red;
for (int i = 0; i < path.NumPoints; i++)
{
Vector2 newPos = Handles.FreeMoveHandle(path[i], Quaternion.identity, .1f, Vector2.zero, Handles.CylinderHandleCap);
if (path[i] != newPos)
{
Undo.RecordObject(creator, "Move point");
path.MovePoint(i, newPos);
}
}
}
void OnEnable()
{
creator = (PathCreator)target;
if (creator.path == null)
{
creator.CreatePath();
}
path = creator.path;
}
}
我觉得你的想法很好:你可以写两个classes,命名为ControlPoint
和HandlePoint
(使它们可序列化)。
ControlPoint
可能代表每条曲线的 p0
和 p3
- 路径确实经过的点。对于连续性,您必须断言一个段的p3
等于下一段的p0
。
HandlePoint
可以表示每条曲线的 p1
和 p2
- 曲线的切点并提供方向和倾斜度。对于 平滑度 ,您必须断言一个段的 (p3 - p2).normalized
等于下一段的 (p1 - p0).normalized
。 (如果你想要 对称平滑度 ,一个的 p3 - p2
必须等于另一个的 p1 - p0
。)
提示#1:在分配或比较每个段的点时始终考虑矩阵变换。我建议你在执行操作之前将任何点转换为全局 space。
提示 #2:考虑在段内的点之间应用约束,因此当您在曲线的 p0
或 p3
周围移动时,p1
或 p2
分别相应地移动相同的量(就像任何图形编辑器软件在贝塞尔曲线上所做的一样)。
编辑 -> 提供的代码
我做了一个想法的示例实现。实际上,在开始编码之后,我意识到只需一个 class ControlPoint
(而不是两个)就可以完成这项工作。 A ControlPoint
有 2 条切线。所需的行为由字段 smooth
控制,可以为每个点设置。
ControlPoint.cs
using System;
using UnityEngine;
[Serializable]
public class ControlPoint
{
[SerializeField] Vector2 _position;
[SerializeField] bool _smooth;
[SerializeField] Vector2 _tangentBack;
[SerializeField] Vector2 _tangentFront;
public Vector2 position
{
get { return _position; }
set { _position = value; }
}
public bool smooth
{
get { return _smooth; }
set { if (_smooth = value) _tangentBack = -_tangentFront; }
}
public Vector2 tangentBack
{
get { return _tangentBack; }
set
{
_tangentBack = value;
if (_smooth) _tangentFront = _tangentFront.magnitude * -value.normalized;
}
}
public Vector2 tangentFront
{
get { return _tangentFront; }
set
{
_tangentFront = value;
if (_smooth) _tangentBack = _tangentBack.magnitude * -value.normalized;
}
}
public ControlPoint(Vector2 position, bool smooth = true)
{
this._position = position;
this._smooth = smooth;
this._tangentBack = -Vector2.one;
this._tangentFront = Vector2.one;
}
}
我还为 ControlPoint
class 编写了一个自定义 PropertyDrawer
,因此它可以更好地显示在检查器上。这只是一个天真的实现。你可以大大改进它。
ControlPointDrawer.cs
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(ControlPoint))]
public class ControlPointDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
int indent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0; //-= 1;
var propPos = new Rect(position.x, position.y, position.x + 18, position.height);
var prop = property.FindPropertyRelative("_smooth");
EditorGUI.PropertyField(propPos, prop, GUIContent.none);
propPos = new Rect(position.x + 20, position.y, position.width - 20, position.height);
prop = property.FindPropertyRelative("_position");
EditorGUI.PropertyField(propPos, prop, GUIContent.none);
EditorGUI.indentLevel = indent;
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight;
}
}
我遵循了与您的解决方案相同的架构,但进行了必要的调整以适应 ControlPoint
class 和其他 fixes/changes。例如,我将所有点值存储在本地坐标中,因此组件或父级上的变换反映在曲线中。
Path.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),
new ControlPoint(position + Vector2.right)
};
}
public bool loop { get { return _loop; } set { _loop = value; } }
public ControlPoint this[int i] { get { return _points[(_loop && i == _points.Count) ? 0 : i]; } }
public int NumPoints { get { return _points.Count; } }
public int NumSegments { get { return _points.Count - (_loop ? 0 : 1); } }
public ControlPoint InsertPoint(int i, Vector2 position, bool smooth)
{
_points.Insert(i, new ControlPoint(position, smooth));
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].position = position;
return this[i];
}
public ControlPoint MoveTangentBack(int i, Vector2 position)
{
this[i].tangentBack = position;
return this[i];
}
public ControlPoint MoveTangentFront(int i, Vector2 position)
{
this[i].tangentFront = position;
return this[i];
}
}
PathEditor
几乎是一回事。
PathCreator.cs
using UnityEngine;
public class PathCreator : MonoBehaviour
{
public Path path;
public Path CreatePath()
{
return path = new Path(Vector2.zero);
}
void Reset()
{
CreatePath();
}
}
最后,所有的魔法都发生在 PathCreatorEditor
。这里有两条评论:
1) 我将线条的绘制移至自定义 DrawGizmo
静态函数,因此即使对象不是 Active
(即在检查器中显示),您也可以绘制线条如果你愿意,甚至可以让它变得可挑选。我不知道你是否想要这种行为,但你可以轻松恢复;
2) 注意 class 上方的 Handles.matrix = creator.transform.localToWorldMatrix
行。它会自动将点的比例和旋转转换为世界坐标。那边也有PivotRotation
的细节
PathCreatorEditor.cs
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(PathCreator))]
public class PathCreatorEditor : Editor
{
PathCreator creator;
Path path;
SerializedProperty property;
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(property, true);
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, false);
}
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 = Tools.pivotRotation == PivotRotation.Local ? creator.transform.rotation : Quaternion.identity;
var snap = Vector2.zero;
Handles.CapFunction cap = Handles.CylinderHandleCap;
for (int i = 0; i < path.NumPoints; i++)
{
var pos = path[i].position;
var size = .1f;
Handles.color = Color.red;
Vector2 newPos = Handles.FreeMoveHandle(pos, rot, size, snap, cap);
if (pos != newPos)
{
Undo.RecordObject(creator, "Move point position");
path.MovePoint(i, newPos);
}
pos = newPos;
if (path.loop || i != 0)
{
var tanBack = pos + path[i].tangentBack;
Handles.color = Color.black;
Handles.DrawLine(pos, tanBack);
Handles.color = Color.red;
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.color = Color.black;
Handles.DrawLine(pos, tanFront);
Handles.color = Color.red;
Vector2 newTanFront = Handles.FreeMoveHandle(tanFront, rot, size, snap, cap);
if (tanFront != newTanFront)
{
Undo.RecordObject(creator, "Move point tangent");
path.MoveTangentFront(i, newTanFront - pos);
}
}
}
}
[DrawGizmo(GizmoType.Selected | GizmoType.NonSelected)]
static void DrawGizmo(PathCreator creator, GizmoType gizmoType)
{
Handles.matrix = creator.transform.localToWorldMatrix;
var path = creator.path;
for (int i = 0; i < path.NumSegments; i++)
{
Vector2[] points = path.GetBezierPointsInSegment(i);
Handles.DrawBezier(points[0], points[3], points[1], points[2], Color.green, null, 2);
}
}
void OnEnable()
{
creator = (PathCreator)target;
path = creator.path ?? creator.CreatePath();
property = serializedObject.FindProperty("path");
}
}
此外,我添加了一个 loop
字段以防您希望曲线闭合,并且我添加了一个简单的功能来通过 Ctrl+click
在场景中删除点。
总而言之,这只是基本的东西,但您可以根据需要进行高级操作。此外,您可以将 ControlPoint class 与其他组件重用,例如 Catmull-Rom 样条、几何形状、其他参数函数...
你的 post 中的基本问题是:'Is it a good idea to a have a separate Class for the points of a bezier curve?'
由于曲线将由这样的点组成,而且这些点不仅仅是两个坐标 imo 这肯定是个好主意。
但是,像往常一样,在进行 class 设计时,让我们收集一些 用例 ,即点将用于的事情或我们期望做的事情一点..:[=17=]
- 可以在曲线上添加或删除点
- 可以移动一个点
- 它的控制点可以移动
除了单纯的位置,一个点,即'anchor point'应该有更多的属性,abilities/methods..:[=17=]
有控制点;这些与要点的关系有时并不完全相同。查看 Unity 文档,我们看到
Handles.DrawLine
查看两个点及其 'inner' 控制点。来自 GDI+GraphicsPath
我看到一系列点,在 1 个锚点和 2 个控制点之间交替。 Imo,这为将两个控制点视为锚点的属性提供了更有力的理由。因为两者都必须是可移动的,所以它们可能有一个共同的祖先或连接到movecontroller
class;但我相信你最了解如何在 Unity 中做到这一点..真正开始的 属性 问题类似于
bool IsContinuous
。当true
我们需要耦合- 移动一个控制点以'the opposite'方式移动另一个控制点。
- 将锚点移动到平行移动两个控制点
- 也许 属性
bool IsLocked
以防止移动它 - 也许 属性
bool IsProtected
以防止在 reducing/simplifying 曲线时删除它。 (这对于构造曲线几乎不需要,但对于徒手绘制或用鼠标追踪的曲线非常需要) - 也许一个属性知道可以一起编辑的一组点中的点。
- 也许是一个通用标记。
- 可能是文本注释
- 可能是表示曲线中 break/split 的类型指示符。
- 可能是增加或减少平滑度与尖锐度的方法。
一些用例显然主要涉及曲线,但其他用例不涉及;有些对两者都有用。
所以,显然我们有很多充分的理由来创建一个聪明的 ÀnchPoint` class..
((我有点忙,但仍计划为 GraphicsPath 贝塞尔曲线编写我自己的编辑器。如果发生这种情况,我会用我学到的东西更新 post,包括class 我想出的设计..))