Как сделать отдельные точки привязки безье непрерывными или непостоянными
Я создаю кривые Безье с помощью следующего кода. Кривые могут быть расширены, чтобы объединить несколько кривых Безье, нажимая клавишу 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;
}
}
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 ответа
Я думаю, что ваша идея в порядке: вы можете написать два класса с именем 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
(вместо двух) сделает работу. 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, геометрические фигуры, другие параметрические функции...
Основной вопрос в вашем посте: "Является ли хорошей идеей иметь отдельный класс для точек кривой Безье?"
Поскольку кривая будет состоять из таких точек, а это не просто две координаты, то, конечно, это хорошая идея.
Но, как обычно при разработке класса, давайте соберем несколько сценариев использования, то есть вещей, для которых будет использоваться точка, или вещей, которые мы ожидаем сделать с точкой..:
- Точка может быть добавлена или удалена из кривой
- Точка может быть перемещена
- Его контрольные точки могут быть перемещены
Помимо простого местоположения, точка, то есть "опорная точка", должна иметь больше свойств и способностей / методов..:
Имеет контрольные точки; как они связаны с точками, иногда не совсем то же самое. Глядя на документы Unity, мы видим, что
Handles.DrawLine
смотрит на две точки и их "внутренние" контрольные точки. Исходя из GDI+GraphicsPath
Я вижу последовательность точек, меняющихся между 1 якорем и 2 контрольными точками. Имо, это делает еще более сильный случай для лечения двух контрольных точек как свойства точки привязки. Так как оба должны быть подвижными, они могут иметь общего предка или быть подключены кmovecontroller
учебный класс; но я верю, что ты лучше знаешь, как это сделать в Unity..Свойство, с которого действительно начался вопрос, было чем-то вроде
bool IsContinuous
, когдаtrue
нам нужно пара- перемещение контрольной точки для перемещения другой "противоположным" способом.
- перемещение якоря для перемещения обеих контрольных точек параллельно
- Может быть собственность
bool IsLocked
чтобы предотвратить его перемещение - Может быть собственность
bool IsProtected
чтобы предотвратить его удаление при уменьшении / упрощении кривой. (Что вряд ли нужно для построенных кривых, но очень важно для кривых от рисования от руки или трассировки с помощью мыши) - Может быть, свойство знать, что точка в группе точек, которые можно редактировать вместе.
- Может быть, общий маркер.
- Может быть текстовая аннотация
- Может быть, индикатор типа, который обозначает разрыв / разделение на кривой.
- Может быть, методы для увеличения или уменьшения гладкости против точечности.
Некоторые варианты использования явно связаны с кривой, а другие - нет; а некоторые полезны для обоих.
Итак, у нас есть много веских причин для создания умного класса ÀnchPoint.
((Я немного запутался, но все же планирую написать свой собственный редактор для кривых Безье GraphicsPath. Если и когда это произойдет, я обновлю пост с учетом того, что я узнал, включая дизайн классов, который я придумал...))