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

 

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