Comment rendre les points d’ancrage individuels de Bézier continus ou non continus

Je crée des courbes de Bézier avec le code suivant. Les courbes peuvent être étendues pour joindre plusieurs courbes de Bézier en cliquant avec la souris sur la vue de la scène. Mon code dispose de fonctionnalités permettant de rendre toute la courbe continue ou non continue. J’ai réalisé que je devais faire en sorte que les points individuels (en particulier les points d’ancrage) possèdent cette fonctionnalité.

Je pense que le moyen le plus idéal d’y parvenir est de créer une nouvelle classe pour les points avec cette fonctionnalité (rendre les points continus ou non continus), car cela peut être utilisé pour append d’autres propriétés pouvant être spécifiques aux points. Comment peut-on faire ça?

Chemin

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

Je pense que votre idée est bonne: vous pouvez écrire deux classes, nommées ControlPoint et HandlePoint (les rendre sérialisables).

ControlPoint peut représenter les points p0 et p3 de chaque courbe – les points que le chemin traverse effectivement. Pour assurer la continuité , vous devez affirmer que p3 d’un segment est égal à p0 du segment suivant.

HandlePoint peut représenter p1 et p2 de chaque courbe – les points qui sont des tangentes à la courbe et qui fournissent une direction et une inclinaison. Pour que le lissage soit lisse , vous devez affirmer que (p3 - p2).normalized d’un segment est égal à (p1 - p0).normalized du segment suivant. (Si vous voulez un lissage symésortingque , p3 - p2 de l’un doit être égal à p1 - p0 de l’autre.)

Conseil n ° 1 : tenez toujours compte des transformations de masortingce lors de l’atsortingbution ou de la comparaison des points de chaque segment. Je vous suggère de convertir n’importe quel point en espace global avant d’effectuer les opérations.

Conseil n ° 2 : envisagez d’appliquer une contrainte entre les points d’un segment. Ainsi, lorsque vous vous déplacez autour de p0 ou p3 d’une courbe, p1 ou p2 déplacent en conséquence de la même quantité, respectivement (comme tout logiciel d’édition graphique sur les courbes de Bézier).


Édition -> Code fourni

J’ai fait un exemple de mise en œuvre de l’idée. En fait, après avoir commencé à coder, je me suis rendu compte que seule une classe ControlPoint (au lieu de deux) ferait l’affaire. Un ControlPoint a 2 tangentes. Le comportement souhaité est contrôlé par le champ smooth , qui peut être défini pour chaque point.

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

J’ai également codé un PropertyDrawer personnalisé pour la classe ControlPoint afin qu’il soit mieux visible sur l’inspecteur. C’est juste une implémentation naïve. Vous pourriez l’améliorer beaucoup.

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

J’ai suivi la même architecture que votre solution, mais avec les ajustements nécessaires pour l’adapter à la classe ControlPoint et d’autres correctifs / modifications. Par exemple, j’ai stocké toutes les valeurs de points en coordonnées locales afin que les transformations sur le composant ou les parents soient reflétées dans la courbe.

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 est à peu près la même chose.

PathCreator.cs

 using UnityEngine; public class PathCreator : MonoBehaviour { public Path path; public Path CreatePath() { return path = new Path(Vector2.zero); } void Reset() { CreatePath(); } } 

Enfin, toute la magie se passe dans PathCreatorEditor . Deux commentaires ici:

1) J’ai déplacé le dessin des lignes vers une fonction statique DrawGizmo personnalisée afin que vous puissiez conserver les lignes même lorsque l’object n’est pas Active (c’est-à-dire affiché dans l’inspecteur). Vous pouvez même le rendre sélectionnable si vous le souhaitez. Je ne sais pas si vous voulez ce comportement, mais vous pouvez facilement revenir en arrière;

2) Notez les lignes Handles.masortingx = creator.transform.localToWorldMasortingx sur la classe. Il transforme automatiquement l’échelle et la rotation des points en coordonnées mondiales. Il y a un détail avec PivotRotation là-bas aussi.

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

De plus, j'ai ajouté un champ de loop au cas où vous voudriez que la courbe soit fermée, et une fonction naïve pour supprimer des points par Ctrl+click sur la scène. En résumé, il ne s’agit que de choses élémentaires, mais vous pouvez le faire aussi bien que vous le souhaitez. Vous pouvez également réutiliser votre classe ControlPoint avec d'autres composants, tels qu'une spline Catmull-Rom, des formes géomésortingques, d'autres fonctions paramésortingques, etc.

La question de base de votre message est la suivante: “Est-ce une bonne idée d’avoir une classe séparée pour les points d’une courbe de Bézier?”

Comme la courbe sera composée de tels points et que ceux-ci sont plus que deux coordonnées, c’est certainement une bonne idée .

Mais, comme d’habitude lors de la conception de classes, rassemblons quelques cas d’utilisation , c’est-à-dire des choses pour lesquelles un point sera utilisé ou des choses que nous comptons faire jusqu’à un point ..

  • Un point peut être ajouté ou supprimé d’une courbe
  • Un point peut être déplacé
  • Son ou ses points de contrôle peuvent être déplacés

Outre le simple emplacement, un point, c’est-à-dire un “point d’ancrage”, doit avoir davantage de propriétés et de capacités / méthodes ..:

  • Il a des points de contrôle; leur relation avec les points n’est parfois pas exactement la même chose. En regardant les documents Unity, nous voyons que Handles.DrawLine examine deux points et leurs points de contrôle «internes». En venant de GDI + GraphicsPath je vois une séquence de points, en alternant 1 point d’ancrage et 2 points de contrôle. De plus, cela plaide encore plus pour le traitement des deux points de contrôle en tant que propriétés du point d’ancrage. Étant donné que les deux doivent être mobiles, ils peuvent avoir un ancêtre commun ou être connectés à movecontroller classe de movecontroller de movecontroller ; mais j’espère que vous savez mieux comment faire cela dans l’unité ..

  • La propriété sur laquelle la question a réellement commencé était quelque chose comme bool IsContinuous . Quand c’est true il faut coupler

    • déplacer un sharepoint contrôle pour déplacer l’autre de manière “opposée”.
    • déplacer l’ancre pour déplacer les deux points de contrôle en parallèle
  • Peut-être une propriété bool IsLocked pour empêcher le déplacer
  • Peut-être une propriété bool IsProtected pour éviter de la supprimer lors de la réduction / simplification de la courbe. (Ce qui est à peine nécessaire pour les courbes construites, mais très bien pour les courbes de dessin à main levée ou de tracé à la souris)
  • Peut-être une propriété de savoir que le point dans un groupe de points qui peuvent être édités ensemble.
  • Peut-être un marqueur général.
  • Peut-être une annotation textuelle
  • Peut-être un indicateur de type indiquant une rupture / scission dans la courbe.
  • Peut-être que des méthodes pour augmenter ou diminuer la douceur par rapport à la ponctualité.

Certains cas d’utilisation impliquent clairement la courbe mais d’autres pas; et certains sont utiles pour les deux.

Nous avons donc de bonnes raisons de créer une classe intelligente ÀnchPoint`.

((Je suis un peu coincé mais je prévois quand même d’écrire mon propre éditeur pour les courbes de Bezier de GraphicsPath. Si cela se produit, je mettrai à jour le post avec ce que j’ai appris, y compris la conception de la classe que je propose ..) )