Unity中的贝塞尔曲线思路及实现(转)

https://blog.csdn.net/GhostOrange/article/details/53613372

 

说到贝塞尔曲线,大家肯定不会陌生,很多插件都有它的身影,但是想要一窥它的细节总觉得有点困难,因为一到网上查询都是什么看不懂的公式啊(大神就不一样了),多项式啊吧啦吧啦的,说起多项式,估计大多数人都是感觉好像似曾相识,但是如果想要根据这些公式什么的来实现贝塞尔曲线,估计非常有难度吧。那今天我就来说个简单版的。

先来看看最终实现效果:

好,首先,让我们先来分析下贝塞尔曲线。

由上图我出的结论就是:

贝塞尔曲线就是:在几条连续的线段中,根据T进行取值,获得这些线段上位于T的点,然后再将这些点链接起来,得到新的线段(这些线段的数量始终比原始线段数量少1),如果这些点只能得到一条线段,那么贝塞尔曲线就是这条线上的T(0-1)位置所有的点连接起来所绘制成的曲线。如果这些点依次连接起来最后得出的点的数量超过了一条,则再次在这些线段上根据T获得新的点再次连接这些点,直到最后得出的线段数量为一。

好啦,以上就是我昨天在突然发现这些漂亮的曲线的时候得出的结论啦,所以我就准备看看能不能根据这个规则把贝塞尔曲线绘制出来。

接下来就是要编程了,如果还没有理解到贝塞尔曲线的规律的话可能在理解程序上会有点懵逼哦。

我们首先从基础的做起,万丈高楼平地起,先封装个“线”:

    /// <summary>
    /// 线段,包含起点和终点
    /// </summary>
    public struct Line{
        public Line(Vector3 start,Vector3 end){
            StartPoint = start;
            EndPoint = end;
        }
        /// <summary>
        /// 线段的起点
        /// </summary>
        /// <value>The start point.</value>
        public Vector3 StartPoint {
            get;
            set;
        }
        /// <summary>
        /// 线段的终点
        /// </summary>
        /// <value>The end point.</value>
        public Vector3 EndPoint{
            get;
            set;
        }
        /// <summary>
        /// 判断一个点是否是自己的起点或者终点
        /// </summary>
        /// <returns><c>true</c>, if me was ised, <c>false</c> otherwise.</returns>
        /// <param name="point">Point.</param>
        public bool isMe(Vector3 point){
            if (StartPoint == point || EndPoint == point) {
                return true;
            } else {
                return false;
            }
        }
        /// <summary>
        /// 根据传入的值获取这条线段上的任意一点,T>=0&&T<=1
        /// T=0时返回起点
        /// T=1时返回终点
        /// </summary>
        /// <returns>The point.</returns>
        /// <param name="T">T.</param>
        public Vector3 GetPoint(float T){
            var point = new Vector3 ();
            if (T < 0) {
                T = 0;
            } else if (T > 1) {
                T = 1;
            }
            point = (EndPoint - StartPoint) * T + StartPoint;
            return point;
        }
    }


好了,线段就封装好了,下面准备搞事情了。

开始编写贝塞尔曲线的核心内容:

/// <summary>
    /// 贝塞尔曲线
    /// </summary>
    public class Bezier :System.Object {
        private List <Vector3> m_Points;
        private List <Line> createdLine;
        /// <summary>
        /// Initializes a new instance of the <see cref="MyTools.Bezier"/> class.
        /// </summary>
        /// <param name="points">Points.</param>
        public Bezier(List<Vector3> points){
            if (points.Count < 2) {
                throw(new ArgumentException ("实例化贝塞尔曲线至少需要2个点"));
            } else {
                m_Points = new List<Vector3> ();
                createdLine = new List<Line> ();
                CreateLine (points);
            }
        }
        #region 修改参数的方法
        public void AddPoint(Vector3 point){
            m_Points.Add (point);
            CreateLine (m_Points);
        }
 
        public void AddPointAt(int index,Vector3 point){
            if (index >= 0 && index < m_Points.Count) {
                m_Points.Insert (index, point);
                CreateLine (m_Points);
            } else {
                throw (new ArgumentOutOfRangeException ("索引超出范围"));
            }
        }
 
        public void RemovePoint(Vector3 point){
            if (m_Points.Count > 2) {
                for (int i = 0; i < m_Points.Count; i++) {
                    if (m_Points [i] == point) {
                        m_Points.RemoveAt (i);
                        CreateLine (m_Points);
                    } else {
                        continue;
                    }
                }
            } else {
                Exception ex = new Exception ("当前曲线锚点数量已经最低,不能移除锚点");
                throw(ex);
            }
        }
 
        public void RemovePointAt(int index){
            if (m_Points.Count > 2) {
                m_Points.RemoveAt (index);
                CreateLine (m_Points);
            } else {
                Exception ex = new Exception ("当前曲线锚点数量已经最低,不能移除锚点");
                throw(ex);
            }
        }
 
        public void UpdatePoint(int ListIndex, Vector3 point){
            if (ListIndex < 0) {
                throw (new ArgumentException ("座标索引参数错误(取值必须大于0)"));
            } else if (ListIndex >= m_Points.Count) {
                throw (new ArgumentException ("座标索引参数错误(取值必须x小于曲线顶点的个数)"));
            } else {
                m_Points [ListIndex] = point;
                CreateLine (m_Points);
            }
        }
 
        #endregion
 
        /// <summary>
        /// 根据传入的参数获取曲线上某一点的值
        /// </summary>
        /// <returns>The point.</returns>
        /// <param name="T">取值参数(0-1).</param>
        public Vector3 GetPoint(float T){
            var point = new Vector3 ();
            if (T < 0) {
                T = 0;
            } else if (T > 1) {
                
                T = 1;
            }
            var bufListLine = createdLine;
            if (bufListLine == null) {
                throw(new NullReferenceException ("曲线锚点为空"));
            }
            while (bufListLine.Count > 1) {
        
                bufListLine = CaculateResoultLine (bufListLine, T);
            }
            if (bufListLine.Count == 1) {
                point = bufListLine [0].GetPoint (T);
            } else {
                throw(new Exception("Program Error : Current Line Count is:   " + bufListLine.Count));
            }
 
            return point;
 
        }
        /// <summary>
        /// 根据当前的线段以及取值参数T,创建新的线段链表(新的链表长度始终等于原始链表长度-1)。
        /// 使用迭代计算的方式降低程序的复杂性
        /// </summary>
        /// <param name="Lines">Lines.</param>
        /// <param name="T">T.</param>
        private List<Line> CaculateResoultLine(List<Line> Lines,float T){
            var ListLine = new List<Line>();
            for (int i = 0; i < Lines.Count-1; i++) {
                var j = i + 1;
                Line bufLine = new Line (Lines [i].GetPoint (T),Lines [j].GetPoint (T));
                ListLine.Add (bufLine);
            }
            return ListLine;
        }
        /// <summary>
        /// 根据已知的锚点依次创建一条连续的折线
        /// </summary>
        /// <param name="points">Points.</param>
        private void CreateLine(List<Vector3> points){
            createdLine = new List<Line> ();
            m_Points = points;
            for (int i = 0; i < points.Count; i++) {
                var j = i + 1;
                if (j >= points.Count) {
                    break;
                } else {
                    Line curLine = new Line (points [i], points [j]);
                    createdLine.Add (curLine);
                }
            }
        }
    }


以上就是整个贝塞尔曲线得核心思想啦,下面开始分解这个程序。

1.首先说下这两个变量:

        private List <Vector3> m_Points; //存储曲线锚点
        private List <Line> createdLine; //存储根据锚点依次连接所得到的线段,以此作为曲线基本的数据源


2.然后是实现“在几条连续的线段中,根据T进行取值,获得这些线段上位于T的点,然后再将这些点链接起来,得到新的线段” 这个功能:
/// <summary>
        /// 根据当前的线段以及取值参数T,创建新的线段链表(新的链表长度始终等于原始链表长度-1)。
        /// 使用迭代计算的方式降低程序的复杂性
        /// </summary>
        /// <param name="Lines">Lines.</param>
        /// <param name="T">T.</param>
        private List<Line> CaculateResoultLine(List<Line> Lines,float T){
            var ListLine = new List<Line>();
            for (int i = 0; i < Lines.Count-1; i++) {
                var j = i + 1;
                Line bufLine = new Line (Lines [i].GetPoint (T),Lines [j].GetPoint (T));
                ListLine.Add (bufLine);
            }
            return ListLine;
        }

3.核心函数,获取曲线上位于T的点
/// <summary>
        /// 根据传入的参数获取曲线上某一点的值
        /// </summary>
        /// <returns>The point.</returns>
        /// <param name="T">取值参数(0-1).</param>
        public Vector3 GetPoint(float T){
            var point = new Vector3 ();
            if (T < 0) {
                T = 0;
            } else if (T > 1) {
                
                T = 1;
            }
            var bufListLine = createdLine;
            if (bufListLine == null) {
                throw(new NullReferenceException ("曲线锚点为空"));
            }
            //注意这里,使用迭代计算的方式计算出最后的线段,如果有5个点,则有4条线,那么这个“while{}”就会执行3次,而每次都会使用for循环遍历线段。
            //所以,当曲线的锚点比较多的时候,此处可能会花费较长的时间
 
            while (bufListLine.Count > 1) {
        
                bufListLine = CaculateResoultLine (bufListLine, T);
            }
            if (bufListLine.Count == 1) {
                point = bufListLine [0].GetPoint (T);
            } else {
                throw(new Exception("Program Error : Current Line Count is:   " + bufListLine.Count));
            }
 
            return point;
 
        }


3.刷新锚点位置,此方法需要手动调用

/// <summary>
        /// 更新锚点位置以调整曲线路径
        /// </summary>
        /// <param name="ListIndex">List index.</param>
        /// <param name="point">Point.</param>
        public void UpdatePoint(int ListIndex, Vector3 point){
            if (ListIndex < 0) {
                throw (new ArgumentException ("座标索引参数错误(取值必须大于0)"));
            } else if (ListIndex >= m_Points.Count) {
                throw (new ArgumentException ("座标索引参数错误(取值必须x小于曲线顶点的个数)"));
            } else {
                m_Points [ListIndex] = point;
                CreateLine (m_Points);
            }
        }

然后核心差不多都在这里啦,其他创建线段的方法啊,修改锚点啊,线段啊什么的就随便理解下吧,那些不重要也不是核心。就不单独提出来弄晕大家了


4.最后就是调用方法啦:

using UnityEngine;
using System.Collections;
using MyTools;
using System.Collections.Generic;
 
public class TestBezier : MonoBehaviour {
    /// <summary>
    /// 曲线锚点的Transform,方便在场景中控制
    /// </summary>
    public List<Transform>  Points;
    /// <summary>
    /// 曲线锚点
    /// </summary>
    private List<Vector3> PointsPositions;
    private Bezier bezierCurve=null;
    /// <summary>
    /// 实例化曲线
    /// </summary>
    void Awake () {
        var PointsPositions = new List<Vector3> ();
        for (int i = 0; i < Points.Count; i++) {
            PointsPositions.Add (Points [i].position);
        }
 
        bezierCurve = new Bezier (PointsPositions);
    }
 
    // Update is called once per frame
    void OnDrawGizmos () {
        Gizmos.color = Color.white;
        //绘制锚点连线
        for (int i = 0; i < Points.Count-1; i++) {
            Gizmos.DrawLine (Points[i].position,Points[i+1].position);
        }
 
        Gizmos.color = Color.red;
        if (bezierCurve == null) {
            return;
        }
        //绘制曲线
        for (int i = 0; i < 50    ; i++) {
            var j = i + 1;
            Gizmos.DrawLine (bezierCurve.GetPoint (i / (float)50), bezierCurve.GetPoint (j / (float)50));
        }
        for (int i = 0; i < Points.Count; i++) {
            bezierCurve.UpdatePoint (i, Points [i].position);
        }
    }
}


然后场景中这样配置下,将TestBezier放到一个物体上,并在Point中放入2个以上的Transform组件,运行即可,并且可以通过拖动锚点看到曲线的实时路径哦


最后补充:

贝塞尔曲线的算法我根本看不懂,公式也看不懂,只能通过看图的方式简单理解下,不对的地方,还请给位多多包涵,指点一二。也算是抛砖引玉,谢过啦~

以上。

————————————————
版权声明:本文为CSDN博主「GGeo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/GhostOrange/java/article/details/53613372

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章