Как сделать отдельные точки привязки безье непрерывными или непостоянными - PullRequest
0 голосов
/ 11 июня 2018

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

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

Path

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

Ответы [ 2 ]

0 голосов
/ 16 июня 2018

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

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

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

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

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

  • Имеет контрольные точки;как они связаны с точками, иногда не совсем то же самое.Глядя на документы Unity, мы видим, что Handles.DrawLine смотрит на две точки и их «внутренние» контрольные точки.Исходя из GDI + GraphicsPath Я вижу последовательность точек, altenrating между 1 якоря и 2 контрольных точек.Имо, это делает еще более сильный случай для лечения двух контрольных точек как свойства точки привязки.Так как оба должны быть подвижными, они могут иметь общего предка или быть подключены до movecontroller класса;но я надеюсь, вы лучше знаете, как это сделать в Unity ..

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

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

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

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

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

0 голосов
/ 11 июня 2018

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

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

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

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

Совет № 2 : рассмотрите возможность применения ограничения между точками внутри сегмента, поэтому при перемещении вокруг p0 илиp3 кривой, p1 или p2 перемещаются соответственно на ту же величину соответственно (как любое графическое программное обеспечение делает на кривых Безье).


Редактировать -> Предоставленный код

Я сделал пример реализации идеи.На самом деле, после начала кодирования я понял, что только один класс 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;
  }
}

Я также закодировал пользовательский 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, геометрические фигуры, другие параметрические функции ...

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