Как сделать отдельные опорные точки безьеров непрерывными или непрерывными

Я создаю кривые Безье со следующим кодом. Кривые могут быть расширены, чтобы соединить несколько кривых Безье с помощью сдвига, щелкнув по сцене. Мой код имеет функциональность для непрерывной или непрерывной кривой всей кривой. Я понял, что мне нужно, чтобы отдельные точки (в частности, опорные точки) имели эту функциональность.

Я считаю, что наиболее идеальным способом для этого является создание нового класса для точек с этой функциональностью (создание точек непрерывным или непрерывным), поскольку это можно использовать для добавления других свойств, которые могут быть специфическими для точек. Как это можно сделать?

Дорожка

[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;
    }
}

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;
}
}

Ответы

Ответ 1

Я думаю, что ваша идея в порядке: вы можете написать два класса с именем ControlPoint и HandlePoint (сделать их сериализуемыми).

ControlPoint может представлять p0 и p3 каждой кривой - точки, по которым путь действительно проходит. Для непрерывности вы должны утверждать, что p3 одного сегмента равно p0 следующего сегмента.

HandlePoint может представлять p1 и p2 каждой кривой - точки, которые являются касательными к кривой, и обеспечивают направление и наклон. Для гладкости вы должны утверждать, что (p3 - p2).normalized на один сегмент, равный (p1 - p0).normalized из следующего сегмента. (если вы хотите симметричную гладкость, p3 - p2 равны p1 - p0 другого).

Совет № 1: всегда учитывайте преобразования матриц при назначении или сравнении точек каждого сегмента. Я предлагаю вам преобразовать любую точку в глобальное пространство перед выполнением операций.

Совет № 2: рассмотрим применение ограничения между точками внутри сегмента, поэтому, когда вы перемещаете arround 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;
  }
}

Я также закодировал собственный PropertyDrawer для класса ControlPoint, поэтому его можно лучше показать инспектору. Это просто наивная реализация. Вы можете улучшить его очень.

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+click на Сцене. Подводя итог, это всего лишь базовый материал, но вы можете сделать это как продвинутый, как вы хотите. Кроме того, вы можете повторно использовать свой класс ControlPoint с другими компонентами, такими как сплайн Catmull-Rom, геометрические фигуры, другие параметрические функции...

Ответ 2

Основной вопрос в вашем посте: "Разве хорошая идея иметь отдельный класс для точек кривой Безье?"

Поскольку кривая будет состоять из таких точек, и это больше, чем две координаты, то это, безусловно, хорошая идея.

Но, как обычно, когда вы занимаетесь дизайном классов, позвольте собрать несколько вариантов использования, то есть вещи, которые будут использоваться для того, что мы будем делать, или вещи, которые мы ожидаем сделать до определенной точки.

  • Точка может быть добавлена или удалена из кривой
  • Точка может быть перемещена
  • Его контрольная точка (точки) могут быть перемещены

Помимо простого места, точка, то есть "опорная точка", должна иметь больше свойств и способностей/методов..:

  • Он имеет контрольные точки; как они связаны с точками, иногда не совсем то же самое. Рассматривая документы Unity, мы видим, что Handles.DrawLine рассматривает две точки и их "внутренние" контрольные точки. Исходя из GDI+ GraphicsPath я вижу последовательность точек, переходящую между 1 якорем и двумя контрольными точками. Имо, это делает еще более сильный случай для лечения двух контрольных точек как свойства точки привязки. Поскольку оба они должны быть подвижными, они могут иметь общего предка или movecontroller классу movecontroller; но я надеюсь, что вы лучше знаете, как это сделать в Unity.

  • Свойство, с которым действительно начался вопрос, было что-то вроде bool IsContinuous. Когда это true нам нужно

    • перемещая контрольную точку, чтобы переместить другую в обратном направлении.
    • перемещение якоря для одновременного перемещения обеих контрольных точек
  • Возможно, свойство bool IsLocked чтобы предотвратить его перемещение
  • Возможно, свойство bool IsProtected предотвращает его удаление при уменьшении/упрощении кривой. (Что вряд ли необходимо для построенных кривых, но очень важно для кривых от свободного рисования или трассировки с помощью мыши)
  • Может быть, свойство знать, что точка в группе точек, которую можно редактировать вместе.
  • Может быть, общий маркер.
  • Возможно, текстовая аннотация
  • Может быть, индикатор типа, который обозначает разрыв/разделение на кривой.
  • Может быть, способы увеличить или уменьшить гладкость или заостренность.

В некоторых случаях использования в основном используется кривая, но другие нет; и некоторые из них полезны для обоих.

Итак, у нас есть много веских причин для создания умного класса ÀnchPoint..

((Я немного привязан, но все еще планирую написать собственный редактор для графических кривых без графики. Если и когда это произойдет, я обновлю сообщение с теми вещами, которые я узнал, включая класс, который я придумал..) )