如何使贝塞尔曲线的单个锚点连续或不连续 [英] How to make individual anchor points of bezier continuous or non-continuous

查看:22
本文介绍了如何使贝塞尔曲线的单个锚点连续或不连续的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用以下代码创建贝塞尔曲线.可以通过在场景视图中移动单击来扩展曲线以连接多条贝塞尔曲线.我的代码具有使整个曲线连续或不连续的功能.我意识到我需要让个别点(特别是锚点)具有此功能.

I am creating bezier curves with the following code. The curves can be extended to join several bezier curves by shift clicking in the scene view. My code has functionality for making the whole curve continuous or non-continuous. I realised that I need to make individual points (specifically anchor points) have this functionality.

我认为最理想的方法是为具有此功能的点创建一个新类(使点连续或不连续),因为这可用于添加可能特定于点的其他属性.怎么能做到这一点?

I believe the most ideal way to go about this is creating a new class for the points with this functionality (making points continuous or non-continuous) since this can be used to add other properties that might be specific to the points. How can do this?

路径

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

推荐答案

我觉得你的想法很好:你可以写两个类,命名为 ControlPointHandlePoint(使它们可序列化).

I think your idea is fine: you can write two classes, named ControlPoint and HandlePoint (make them serializable).

ControlPoint 可以代表每条曲线的 p0p3 - 路径确实通过的点.为了连续性,您必须断言一个段的p3等于下一段的p0.

ControlPoint may represent p0 and p3 of each curve - the points the path indeed pass through. For continuity, you must assert that p3 of one segment equals to p0 of the next segment.

HandlePoint 可以表示每条曲线的 p1p2 - 作为曲线的切线并提供方向和倾斜度的点.对于平滑度,您必须断言一个片段的(p3 - p2).normalized 等于下一个片段的(p1 - p0).normalized分割.(如果你想要对称平滑,一个的 p3 - p2 必须等于另一个的 p1 - p0.)

HandlePoint may represent p1 and p2 of each curve - the points that are tangents of the curve and provide direction and inclination. For smoothness, you must assert that (p3 - p2).normalized of one segment equals to (p1 - p0).normalized of the next segment. (if you want symetric smoothness, p3 - p2 of one must equals p1 - p0 of the other.)

提示 #1:在分配或比较每个段的点时始终考虑矩阵变换.我建议您在执行操作之前将任意点转换为全局空间.

Tip #1: Always consider matrix transformations when assigning or comparing points of each segment. I suggest you to convert any point to global space before performing the operations.

提示 #2:考虑在段内的点之间应用约束,因此当您在曲线的 p0p3 周围移动时,p1p2 分别相应地移动相同的量(就像任何图形编辑器软件对贝塞尔曲线所做的一样).

Tip #2: consider applying a constraint between points inside a segment, so when you move arround p0 or p3 of a curve, p1 or p2 move accordingly by the same amount, respectively (just like any graphics editor software do on bezier curves).

我对这个想法做了一个示例实现.实际上,在开始编码后,我意识到只有一个类 ControlPoint(而不是两个)就可以完成这项工作.ControlPoint 有 2 个切线.所需的行为由字段 smooth 控制,可以为每个点设置.

I did a sample implementation of the idea. Actually, after start coding I realized that just one class ControlPoint (instead of two) will do the job. A ControlPoint have 2 tangents. The desired behaviour is controled by the field smooth, that can be set for each 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;
  }
}

我还为 ControlPoint 类编写了一个自定义的 PropertyDrawer,因此它可以在检查器上更好地显示.这只是一个幼稚的实现.你可以改进很多.

I also coded a custom PropertyDrawer for the ControlPoint class, so it can be shown better on the inspector. It is just a naive implementation. You could improve it very much.

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 类,以及其他修复/更改.例如,我将所有点值存储在本地坐标中,因此组件或父级上的变换反映在曲线中.

I followed the same architecture of your solution, but with the needed adjustments to fit the ControlPoint class, and other fixes/changes. For example, I stored all the point values in local coordinates, so the transformations on the component or parents reflect in the curve.

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 中.这里有两条评论:

Finally, all the magic happens in the PathCreatorEditor. Two comments here:

1) 我将线条的绘制移动到自定义的 DrawGizmo 静态函数,因此即使对象不是 Active(即显示在检查员)如果您愿意,您甚至可以将其设为可选.我不知道您是否想要这种行为,但您可以轻松恢复;

1) I moved the drawing of the lines to a custom DrawGizmo static function, so you can have the lines even when the object is not Active (i.e. shown in the Inspector) You could even make it pickable if you want to. I don't know if you want this behaviour, but you could easily revert;

2) 注意类上的 Handles.matrix = creator.transform.localToWorldMatrix 行.它会自动将点的缩放和旋转转换为世界坐标.那边也有 PivotRotation 的细节.

2) Notice the Handles.matrix = creator.transform.localToWorldMatrix lines over the class. It automatically transforms the scale and rotation of the points to the world coordinates. There is a detail with PivotRotation over there too.

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 样条、几何形状、其他参数函数...

Moreover, I added a loop field in case you want the curve to be closed, and I added a naive funcionality to remove points by Ctrl+click on the Scene. Summing up, this is just basic stuff, but you could do it as advanced as you want. Also, you can reuse your ControlPoint class with other Components, like a Catmull-Rom spline, geometric shapes, other parametric functions...

这篇关于如何使贝塞尔曲线的单个锚点连续或不连续的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆