1. 程式人生 > 其它 >Unity中畫Bezier貝塞爾曲線(二階、三階)

Unity中畫Bezier貝塞爾曲線(二階、三階)

一、前言

本文轉載 https://blog.csdn.net/linxinfa/article/details/116808549



二、最終效果

1、Unity演示效果

2、Unity Demo原始碼工程
本文Demo工程已上傳到CodeChina,感興趣的同學可自行下載學習。
地址:https://codechina.csdn.net/linxinfa/unitybeziercurvedrawdemo
注:我使用的Unity版本:Unity 2020.1.14f1c1 (64-bit)。

三、貝塞爾曲線原理
貝塞爾曲線(Bezier curve),又稱 貝茲曲線 或 貝濟埃曲線 ,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,當時主要用於汽車主體設計。現主要應用於二維圖形應用程式的數學曲線。一般的向量圖形軟體通過它來精確畫出曲線,比如PhotoShop中的鋼筆工具。

PhotoShop中的鋼筆工具是一個三階的貝塞爾曲線。

1、什麼是階(次)
我們說的三階貝塞爾曲線,三階是什麼意思?
兩種理解:
1、貝塞爾曲線,它的背後是一個數學函式,N階可以理解為N次方的意思,我們也可以把三階貝塞爾曲線叫做三次貝塞爾曲線。
2、二階貝塞爾曲線就是在一階貝塞爾曲線的基礎上再求一次一階貝塞爾曲線;三階貝塞爾曲線就是在二階貝塞爾曲線的基礎上再求一次一階貝塞爾曲線;以此類推。

我覺得第二種理解更準確一點。

2、一階貝塞爾曲線(一元一次函式,線性函式)
我們先來看看 一階貝塞爾曲線 的公式:
給定點P1 P2,函式推導式如下:

一階貝塞爾曲線公式:B(t) = P1 + (P2 − P1)t = P1(1−t)+ P2t, t∈[0,1]

動態效果如下:

注:應該會有同學想問下圖這個動態效果是用什麼軟體做的,是使用js寫的,感興趣的同學可以跳到文章第四節:貝塞爾曲線本地實驗

從動態效果看,我們可以看出,一階貝塞爾曲線其實就是一條線性的線段。

3、二階貝塞爾曲線(一元二次函式)
二階貝塞爾曲線的路徑由給定點P1 、P2、P3 的函式B(t)給出:

二階貝塞爾曲線公式:B(t) = P1(1 - t)2 + 2P2t(1 - t) + P3t2,t∈[0,1]

動態效果如下:

 我們可以拆解一下這個過程,從P1到P2是一個一階貝塞爾曲線過程,從P2到P3也是一個一階貝塞爾曲線過程,這兩個過程同時進行,得到一條新的線段MN。

在MN上再同時進行一階貝塞爾曲線過程,這樣得到的,就是二階貝塞爾曲線了。
這樣,我們可以把二階貝塞爾曲線的公式拆解一下:

M(t) = P1(1 - t) + P2t
N(t) = P2(1 - t) + P3t
B(t) = M(1 - t) + Nt

我們將M和N帶入B(t)函式中,得到的就是:

B(t) = (1 - t)( (1 - t)P1 + tP2) + t((1 - t)P2 + tP3)

化簡一下,就是:

B(t) = P1 - 2P1t + P1t2 + 2P2t - 2P1t2 + P3t3

再整理一下,就是:

B(t) = P1(1 - t)2 + 2P2t(1 - t) + P3t2,

這正是我們上面一開始列的二階貝塞爾曲線的公式。

4、三階貝塞爾曲線
根據一階、二階的貝塞爾曲線的原理,相信大家已經知道三階貝塞爾曲線的推導了吧。

三階貝塞爾曲線公式:B(t) = P1(1 - t)3 + 3P2t(1 - t)2 + 3P3t2(1 - t) + P4t3,t∈[0,1]

動態效果如下:

 寫到這裡了,我就順便推導一下好了,先標一下點,如下:

我們先對X、Y、Z三點分別做一階貝塞爾,得到:

X(t) = P1(1 - t) + P2t
Y(t) = P2(1 - t) + P3t
Z(t) = P3(1 - t) + P4t

接著我們對M、N兩點分別做一階貝塞爾,得到:

M(t) = X(1 - t) + Yt
N(t) = Y(1 - t) + Zt

帶入X、Y、Z,得到:

M(t) = P1(1 - t)2 + 2P2t(1 - t) + P3t2,
N(t) = P2(1 - t)2 + 2P3t(1 - t) + P4t2,

到這裡就可以看出,其實M(t)和N(t)就是二階貝塞爾,三階貝塞爾就是在二階貝塞爾的基礎上再求一次一階貝塞爾。

對B點做一階貝塞爾:

B(t) = M(1 - t) + Nt

帶入M和N,得到公式:

B(t) = (P1(1 - t)2 + 2P2t(1 - t) + P3t2)(1 - t) + (P2(1 - t)2 + 2P3t(1 - t) + P4t2)t

化簡併整理後,最終公式:

B(t) = P1(1 - t)3 + 3P2t(1 - t)2 + 3P3t2(1 - t) + P4t3,t∈[0,1]

5、測試50階貝塞爾曲線
更高階的貝塞爾曲線的公式推導就不一一寫了,我們來玩個猛的,50階貝塞爾曲線:

四、貝塞爾曲線本地實驗
1、工程地址
上文中我演示的貝塞爾曲線動態效果,是通過GitHub的一個開源專案進行演示的。
GitHub源地址:https://github.com/Aaaaaaaty/bezierMaker.js

我在它的基礎上做了一些改進,上傳到了CodeChina,感興趣的可以下載我的版本:
CodeChina地址:https://codechina.csdn.net/linxinfa/beziermaker

2、執行

下載下來後,直接用瀏覽器開啟bezierMaker.html即可。

五、 貝塞爾曲線線上玩

感受到了數學之美了嗎?感興趣的同學可以自己玩一下。我發幾個可以線上實驗貝塞爾曲線的網址給大家吧。

1、貝塞爾曲線路徑運動模擬

地址:https://csdjk.github.io/bezierPathCreater.github.io/

2、貝塞爾曲線畫線練習

地址:https://bezier.method.ac/

3、三次貝塞爾曲線緩動演示(css)

地址:https://cubic-bezier.com/

4、HTML5貝塞爾曲線程式碼生成器(canvas)

地址:http://wx.karlew.com/canvas/bezier/

六、Unity實現貝塞爾曲線

1、貝塞爾曲線C#程式碼:BezierCurve.cs

上面囉嗦了這麼多,我終於要講Unity部分啦。上面的原理懂了之後,其實貝塞爾曲線的演算法程式碼就不難了。
假設我們現在有個控制點的Transform陣列。

Transform[] points;

  那麼,一階貝塞爾曲線的演算法就是:

public Vector3 lineBezier(float t)
{
    Vector3 a = points[0].position;
    Vector3 b = points[1].position;
    return a + (b - a) * t;
}

  二階貝塞爾曲線的演算法:

// 二階貝塞爾曲線
public Vector3 quardaticBezier(float t)
{
    Vector3 a = points[0].position;
    Vector3 b = points[1].position;
    Vector3 c = points[2].position;

    Vector3 aa = a + (b - a) * t;
    Vector3 bb = b + (c - b) * t;
    return aa + (bb - aa) * t;
}

  三階貝塞爾曲線的演算法:

public Vector3 cubicBezier(float t)
{
    Vector3 a = points[0].position;
    Vector3 b = points[1].position;
    Vector3 c = points[2].position;
    Vector3 d = points[3].position;

    Vector3 aa = a + (b - a) * t;
    Vector3 bb = b + (c - b) * t;
    Vector3 cc = c + (d - c) * t;

    Vector3 aaa = aa + (bb - aa) * t;
    Vector3 bbb = bb + (cc - bb) * t;
    return aaa + (bbb - aaa) * t;
}

  把演算法封裝BezierCurve指令碼中。

 BezierCurve.cs完整程式碼:

// BezierCurve.cs
using UnityEngine;

/// <summary>
/// 貝塞爾曲線
/// </summary>
[ExecuteInEditMode]
public class BezierCurve : MonoBehaviour
{
    /// <summary>
    /// 控制點(包括起始點和終止點)
    /// </summary>
    [SerializeField]
    Transform[] points;

    /// <summary>
    /// 精確度
    /// </summary>
    [SerializeField]
    int accuracy = 20;

    void Update()
    {
        // 繪製貝塞爾曲線
        Vector3 prev_pos = points[0].position;
        for (int i = 0; i <= accuracy; ++i)
        {
            Vector3 to = formula(i / (float)accuracy);
            Debug.DrawLine(prev_pos, to);
            prev_pos = to;
        }
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.white;
        // 繪製控制點(包括起始點和終止點)
        for (int i = 0; i < points.Length; ++i)
        {
            if (i < points.Length - 1)
            {
                if (4 == points.Length && i == 1)
                {
                    continue;
                }
                Vector3 current = points[i].position;
                Vector3 next = points[i + 1].position;
                
                Gizmos.DrawLine(current, next);
            }
        }
    }

    /// <summary>
    /// 貝塞爾時間公式(二階、三階)
    /// </summary>
    /// <param name="t">時間引數,範圍0~1</param>
    /// <returns></returns>
    public Vector3 formula(float t)
    {
        switch(points.Length)
        {
            case 3: return quardaticBezier(t);
            case 4: return cubicBezier(t);
        }
        return Vector3.zero;
    }

    /// <summary>
    /// 一階貝塞爾
    /// </summary>
    /// <param name="t">時間引數,範圍0~1</param>
    /// <returns></returns>

    public Vector3 lineBezier(float t)
    {
        Vector3 a = points[0].position;
        Vector3 b = points[1].position;
        return a + (b - a) * t;
    }

    /// <summary>
    /// 二階貝塞爾
    /// </summary>
    /// <param name="t">時間引數,範圍0~1</param>
    /// <returns></returns>
    public Vector3 quardaticBezier(float t)
    {
        Vector3 a = points[0].position;
        Vector3 b = points[1].position;
        Vector3 c = points[2].position;

        Vector3 aa = a + (b - a) * t;
        Vector3 bb = b + (c - b) * t;
        return aa + (bb - aa) * t;
    }

    /// <summary>
    /// 三階貝塞爾
    /// </summary>
    /// <param name="t">時間引數,範圍0~1</param>
    /// <returns></returns>
    public Vector3 cubicBezier(float t)
    {
        Vector3 a = points[0].position;
        Vector3 b = points[1].position;
        Vector3 c = points[2].position;
        Vector3 d = points[3].position;

        Vector3 aa = a + (b - a) * t;
        Vector3 bb = b + (c - b) * t;
        Vector3 cc = c + (d - c) * t;

        Vector3 aaa = aa + (bb - aa) * t;
        Vector3 bbb = bb + (cc - bb) * t;
        return aaa + (bbb - aaa) * t;
    }
}

2、使用LineRenderer繪製曲線

曲線的繪製,我使用了LineRenderer元件,為了讓效果看起來好看一點,我調了個彩虹漸變色,我把寬度調成兩邊細中間粗,這樣看起來更加立體。

3、製作貝塞爾曲線預設

我分別製作了二階貝塞爾曲線和三階貝塞爾曲線的預設。
二階貝塞爾曲線預設:

 三階貝塞爾曲線預設:

4、更新LineRenderer點座標:LineRendererCtrler.cs

預設上掛的元件如下:

 其中BezierCurve是貝塞爾曲線的演算法邏輯,LineRendererCtrler是根據BezierCurve的演算法實時更新LineRenderer元件的座標點。
更新LineRenderer點座標的介面如下:

// LineRenderer.cs

// 設定LineRenderer點數量
public int positionCount { get; set; }
// 設定LineRenderer點座標
public void SetPosition(int index, Vector3 position);

  在預設上把控制點賦值給BezierCurve元件的points陣列,三階貝塞爾曲線有4個控制點,二階貝塞爾曲線的話則是3個控制點。

 LineRendererCtrler.cs完整程式碼:

// LineRendererCtrler.cs
using UnityEngine;

/// <summary>
/// LineRenderer控制器
/// </summary>
[RequireComponent(typeof(LineRenderer))]
[RequireComponent(typeof(BezierCurve))]
public class LineRendererCtrler : MonoBehaviour
{
    [SerializeField]
    int nodeCount = 20;

    [SerializeField]
    LineRenderer lineRenderer;

    [SerializeField]
    BezierCurve bezier;

    void Awake()
    {
        lineRenderer.positionCount = nodeCount + 1;
    }

    void Update()
    {
        // 更新LineRenderer的點
        for (int i = 0; i <= nodeCount; ++i)
        {
            Vector3 to = bezier.formula(i / (float)nodeCount);
            lineRenderer.SetPosition(i, to);
        }
    }
} 

5、滑鼠控制控制點:PointHandle.cs

控制點我用的是球體,我使用了射線檢測來判斷滑鼠是否點中了控制點。
示例:

if (Input.GetMouseButtonDown(0))
{
	Ray ray = cam.ScreenPointToRay(Input.mousePosition);
	RaycastHit hit;
	if (Physics.Raycast(ray, out hit, 100))
	{
	    // 點中了控制點
		targetTrans = hit.transform;
	}
}

  滑鼠移動的時候,要把滑鼠的螢幕座標轉換為世界座標再更新控制點(球體)的座標。
示例:

if (null != targetTrans && Input.GetMouseButton(0))
{
	var targetPos = cam.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, 
 								Input.mousePosition.y, posZ));
	targetTrans.position = targetPos;
}

  滑鼠控制控制點的邏輯,我封裝在PointHandle.cs指令碼中。

控制點掛上PointHandle指令碼。

PointHandle.cs完整程式碼:

using UnityEngine;

/// <summary>
/// 控制點
/// </summary>
public class PointHandle : MonoBehaviour
{
    private Transform targetTrans;
    private Camera cam;
    private float posZ;

    private void Start()
    {
        cam = Camera.main;
    }

    private void Update()
    {
        // 滑鼠左鍵按下
        if (Input.GetMouseButtonDown(0))
        {
            Ray ray = cam.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100))
            {
                // 快取射線碰撞到的物體
                targetTrans = hit.transform;
                // 快取物體與攝像機的距離
                posZ = targetTrans.position.z - cam.transform.position.z;
            }
        }
        // 滑鼠左鍵抬起
        if (Input.GetMouseButtonUp(0))
        {
            // 釋放碰撞體快取
            targetTrans = null;
        }
        // 滑鼠按住中
        if (null != targetTrans && Input.GetMouseButton(0))
        {
            // 滑鼠的螢幕座標轉成世界座標
            // 由於滑鼠的螢幕座標的z軸是0,所以需要使用物體距離攝像機的距離為z周的值
            var targetPos = cam.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, posZ));

            targetTrans.position = targetPos;
        }
    }
}

6、UI互動:UICtrler.cs

簡單搭建一下場景,製作一下介面

寫個介面邏輯指令碼UICtrler.cs

UICtrler.cs掛在Canvas節點上,並賦值對應成員。

UICtrler.cs程式碼如下:

// UICtrler.cs
using UnityEngine;
using UnityEngine.UI;

public class UICtrler : MonoBehaviour
{
    /// <summary>
    /// 二階貝塞爾曲線
    /// </summary>
    public GameObject bezierCurve2;
    /// <summary>
    /// 三階貝塞爾曲線
    /// </summary>
    public GameObject bezierCurve3;

    public Toggle toggleBezier3;

    void Start()
    {
        toggleBezier3.onValueChanged.AddListener((v) => 
        {
            bezierCurve3.SetActive(v);
            bezierCurve2.SetActive(!v);
        });

        // 預設顯示三階貝塞爾曲線
        bezierCurve3.SetActive(true);
        bezierCurve2.SetActive(false);
    }
}

7、執行測試

最終執行測試效果如下: