如何使贝塞尔曲线的个别锚点连续或不连续

18
我正在使用以下代码创建贝塞尔曲线。通过在场景视图中进行Shift+单击,可以将曲线扩展为连接多个贝塞尔曲线。我的代码具有使整个曲线连续或非连续的功能。我意识到我需要使单个点(特别是锚点)具有此功能。 我认为最理想的方法是为这些点创建一个新类,并添加此功能(使点连续或非连续),因为这可以用于添加可能特定于点的其他属性。如何实现? 路径
[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;
}
}

如果点及其两个控制点都在同一条直线上,我认为曲线是连续的。因此,如果其中任何一个移出该直线,另一个或者两个都必须进行调整。 - TaW
@TaW 这是真的。如上面的代码所示,我已经能够为整个曲线(无论控制点和锚点的数量)提供此功能。我的问题是我没有考虑仅针对单个锚点执行此操作。我需要以智能方式完成此操作,最好是使用新的“ControlPoints”或“Points”类,以便将来可以扩展其他功能。 - SuperHyperMegaSomething
2
我明白了。我认为我会使用一个AnchorPoint类,其中包括两个ControlPoints,链接到路径上的相邻AnchorPoints,可能是路径本身,当前状态和限制条件。-像往常一样,我建议列出尽可能多的用例清单。也许所有点都应该继承自“可移动点”类。 - TaW
2
@TaW,那应该非常好用。如果您能基于您的建议和我提供的代码提供一个带有示例代码的答案,以保持连续性和非连续性,我将不胜感激。 - SuperHyperMegaSomething
2
好的,我会尝试撰写一个答案来增加池子里的内容。事实上,这个主题让我非常着迷,我正在编写一个小路径编辑器,以便与GDI+ GraphicsPath一起使用。为此,我想需要一个或两个点类。结果可能也适用于您的框架.. - 我会随时向您报告进展情况.. - TaW
显示剩余2条评论
2个回答

9
我认为你的想法很好:你可以编写两个类,分别命名为ControlPointHandlePoint(使它们可序列化)。 ControlPoint可以表示每条曲线的p0p3 - 路径确实通过的点。为了保持连续性,你必须确保一个段的p3等于下一个段的p0HandlePoint可以表示每条曲线的p1p2 - 是曲线的切线,提供方向和倾斜度。为了保持平滑性,你必须确保一个段的(p3-p2).normalized等于下一个段的(p1-p0).normalized。(如果你想要对称平滑,则一个段的p3-p2必须等于另一个段的p1-p0。) 提示#1:在分配或比较每个段的点时,始终考虑矩阵变换。我建议你在执行操作之前将任何点转换为全局空间。 提示#2:考虑在段内的点之间应用约束,这样当你移动曲线的p0p3时,相应地移动p1p2>(就像任何图形编辑软件在贝塞尔曲线上所做的那样)。

Edit -> Code provided

我对这个想法进行了一个示例实现。实际上,在开始编码后,我意识到只需要一个类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,以便在检查器中更好地显示它。这只是一个天真的实现,您可以大大改进它。 < p >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<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) 注意在类上的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 + 单击”来删除点。 总之,这只是基础知识,但您可以尽可能地使其更加先进。此外,您可以将ControlPoint类与其他组件一起重复使用,例如Catmull-Rom样条线,几何形状,其他参数函数...

1
在我看来,你似乎走在了正确的轨道上,只是用户想要代码。如果你有更新计划,你可能需要在悬赏过期之前考虑这样做 :) - Hilarious404
1
@RodrigoRodrigues 哇,这太神奇了。 我刚刚测试了一下,非常感谢。只有两个问题:1.曲线本身将具有多个属性(在PathCreator中),我需要访问它们。 PropertyDrawer隐藏了所有这些属性。我计划为控制点添加更多属性。我认为能够选择一个控制点(手柄)以从默认检查器更改为单个控制点的PropertyDrawer会更好。1/2(续在下面评论中) - SuperHyperMegaSomething
5
@RodrigoRodrigues 我不认为我理解如何使用“其他方法是在PathCreatorEditor上编写一个可选的小工具,将当前点存储在静态字段中,并使自定义检查器显示有关该点的信息”会起作用。是否有一种方法可以使ControlPoints可选择,以便在单击时在检查器中显示其各自控制点的属性,而无需使ControlPoints成为游戏对象? - SuperHyperMegaSomething
1
@SuperHyperMegaSomething,你能让“选择句柄”在检查器中显示属性吗?之前尝试过类似的操作,但没有成功。 - NSSwift
1
@RodrigoRodrigues 这是一个非常好的回答。我迫切需要像这样的东西。我唯一的问题是,个人而言,我更喜欢使用结构体(正如您在其中一条评论中提到的),并保留点移动的方式,就像原始问题中那样。我觉得这就是超级超级超级什么想要的,只是这么说。这个答案已经很长了。你有计划在Github或Paste Bin上提供类似的东西吗?如果没有,我恳求你提供。 - TenOutOfTen
显示剩余23条评论

3
您的帖子中的基本问题是:“为贝塞尔曲线的点单独创建一个类是否是一个好主意?” 由于曲线将由这些点组成,而这些点不仅仅是两个坐标,因此我认为这绝对是个好主意
但是,在进行类设计时,通常情况下,让我们收集一些用例,即点将被用于的事物或我们期望对点执行的操作..:
  • 可以从曲线中添加或删除点
  • 可以移动点
  • 可以移动其控制点
除了纯粹的位置之外,一个点,即“锚点”,应该具有更多的属性和能力/方法..:
  • 它有控制点;这些与点相关的方式有时并不完全相同。查看Unity文档,我们可以看到Handles.DrawLine查看两个点及其“内部”控制点。从GDI+ GraphicsPath来看,我看到了一系列点,交替出现在1个锚点和2个控制点之间。在我看来,这使得将两个控制点视为锚点属性的情况更加强烈。由于两者都必须可移动,因此它们可能具有共同的祖先或连接到movecontroller类;但我相信您最了解如何在Unity中实现它..

  • 问题真正开始的属性是类似于bool IsContinuous。当true时,我们需要将其与其他控制点“相反”方式移动。

    • 将控制点移动到以“相反”的方式移动另一个控制点。
    • 将锚点移动到并行移动两个控制点
  • 可能有一个bool IsLocked属性来防止移动它
  • 可能有一个bool IsProtected属性,以防止在减少/简化曲线时删除它。(对于构造曲线几乎不需要,但对于用鼠标自由绘制或跟踪的曲线非常重要)
  • 可能有一个属性,以知道可以一起编辑的一组点中的点。
  • 可能有一个通用标记。
  • 可能有一个文本注释
  • 可能有一个类型指示器,表示曲线中的断裂/分裂。
  • 可能有方法来增加或减少平滑度和尖锐度。
某些用例明显大多涉及曲线,但其他用例则不是;有些对两者都有用。
因此,显然我们有很多充分的理由创建一个聪明的“锚点”类..

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接