如何制作bezier连续或不连续的单个锚点
我正在使用以下代码创建贝塞尔曲线。 通过在场景视图中移动单击,可以扩展曲线以连接多个贝塞尔曲线。 我的代码具有使整个曲线连续或不连续的function。 我意识到我需要使单个点(特别是锚点)具有此function。
我认为最理想的方法是为具有此function的点创建一个新类(使点连续或不连续),因为这可以用于添加可能特定于点的其他属性。 怎么办呢?
路径
[System.Serializable] public class Path { [SerializeField, HideInInspector] List points; [SerializeField, HideInInspector] public bool isContinuous; public Path(Vector2 centre) { points = new List { 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 = 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; } }
PathCreator
public class PathCreator : MonoBehaviour { [HideInInspector] public Path path; public void CreatePath() { path = new Path(transform.position); } }
PathEditor
[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; } }
我认为您的想法很好:您可以编写两个类,名为ControlPoint
和HandlePoint
(使它们可序列化)。
ControlPoint
可以表示每条曲线的p0
和p3
– 路径确实通过的点。 对于连续性 ,您必须断言一个段的p3
等于下一个段的p0
。
HandlePoint
可以表示每条曲线的p1
和p2
– 这些曲线是曲线的切线并提供方向和倾斜度。 为了平滑 ,您必须断言(p3 - p2).normalized
。一个段的(p3 - p2).normalized
等于(p1 - p0).normalized
。下一个段的(p1 - p0).normalized
。 (如果你想要对称平滑 ,那么一个p3 - p2
必须等于另一个p1 - p0
。)
提示#1 :在分配或比较每个分段的点时,始终考虑矩阵变换。 我建议你在执行操作之前将任何点转换为全局空间。
提示#2 :考虑在段内的点之间应用约束,因此当您移动曲线的p0
或p3
时, p1
或p2
分别相应地移动相同的量(就像任何图形编辑器软件在贝塞尔曲线上所做的那样)。
编辑 – >提供代码
我做了一个想法的示例实现。 实际上,在开始编码后,我意识到只有一个类ControlPoint
(而不是两个)才能完成这项工作。 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
类编写了一个自定义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
类,以及其他修复/更改。 例如,我将所有点值存储在局部坐标中,因此组件或父项上的变换会反映在曲线中。
Path.cs
using System; using UnityEngine; using System.Collections.Generic; [Serializable] public class Path { [SerializeField] List _points; [SerializeField] bool _loop = false; public Path(Vector2 position) { _points = new List { 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)注意类上的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
字段,以防你想要关闭曲线,我添加了一个天真的function来通过Ctrl+click
场景来删除点。 总而言之,这只是基本的东西,但你可以按照你想要的那样先进。 此外,您可以将ControlPoint类与其他组件一起重用,例如Catmull-Rom样条曲线,几何形状,其他参数函数......
你post中的基本问题是: ‘对于贝塞尔曲线的点有一个单独的类是不是一个好主意?’
由于曲线将由这些点组成,并且这些不仅仅是两个坐标, 因此最肯定是一个好主意 。
但是,像往常一样,在进行类设计时,让我们收集一些用例 ,即一个点将被用于的东西或我们期望做的事情……:
除了单纯的位置,一个点,即一个’锚点’应该有更多的属性和能力/方法..:
bool IsLocked
阻止移动它 bool IsProtected
以防止在减少/简化曲线时删除它。 (对于构造曲线几乎不需要,但对于自由手绘图或使用鼠标进行跟踪的曲线非常需要) 有些用例显然主要涉及曲线,但有些则没有; 有些对两者都有用。
所以,显然我们有很多很好的理由来创建一个聪明的ÀnchPoint`类。
((我有点束缚,但仍计划为GraphicsPath bezier曲线编写我自己的编辑器。如果发生这种情况,我将用我学到的东西更新post,包括我提出的类设计..) )
上述就是C#学习教程:如何制作bezier连续或不连续的单个锚点分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注---计算机技术网(www.ctvol.com)!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/cdevelopment/998699.html