草庐IT

c# - 如何使贝塞尔曲线的单个 anchor 连续或不连续

coder 2023-07-13 原文

我正在使用以下代码创建贝塞尔曲线。通过在场景 View 中按住 Shift 单击,可以扩展曲线以连接多条贝塞尔曲线。我的代码具有使整个曲线连续或不连续的功能。我意识到我需要让各个点(特别是 anchor )具有此功能。

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

路径

[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(使它们可序列化)。

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

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

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

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


编辑 -> 代码提供

我做了一个想法的示例实现。实际上,在开始编码之后,我意识到只需一个类 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;
  }
}

我还为 ControlPoint 类编写了自定义 PropertyDrawer,因此它可以更好地显示在检查器上。这只是一个天真的实现。您可以大大改进它。

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

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

关于c# - 如何使贝塞尔曲线的单个 anchor 连续或不连续,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50788120/

有关c# - 如何使贝塞尔曲线的单个 anchor 连续或不连续的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

    我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  10. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

随机推荐