1. 程式人生 > >Unity 曲線插值(Hermite插值和Catmull_Rom插值)

Unity 曲線插值(Hermite插值和Catmull_Rom插值)

 

1.三次Hermite樣條

        埃爾米特插值時頗為常用的插值演算法,其根本也是三次貝塞爾曲線,有關貝塞爾曲線的知識可以參考這篇文章,有動圖,看起來非常直觀https://www.cnblogs.com/hnfxs/p/3148483.html下面是三次貝塞爾曲線模擬和公式

其中,P0和P3是一條曲線段的起點和終點,P1和P2是這個曲線段的兩個外控制點。

        三次Hermite差值實際上是貝塞爾曲線的轉型,它將兩個外控制點轉成了兩個切線,維基百科對Cubic Hermite spline解釋比較清楚,貼上鍊接以供參考

https://en.wikipedia.org/wiki/Cubic_Hermite_spline。下為三次Hermite 樣條曲線的公式

\boldsymbol{p}(t) = (2t^3-3t^2+1)\boldsymbol{p}_0 + (t^3-2t^2+t)\boldsymbol{m}_0 + (-2t^3+3t^2)\boldsymbol{p}_1 +(t^3-t^2)\boldsymbol{m}_1,t∈[0,1]

P0和P1為曲線段的起點和終點,M0和M1為起點和終點的切線。

2.Catmull-Rom Spline

              原理可參考http://www.dxstudio.com/guide_content.aspx?id=70a2b2cf-193e-4019-859c-28210b1da81f

         

         注意,上圖的四個點只能模擬出P1到P2,之間的曲線,在實際運用中,除了給的一組關鍵點以外,我們還需要給這組的收尾各新增一個點以畫出整個曲線的第一個和最後一個曲線段。同樣,貼上公式模擬P1到P2曲線的公式

為了擬合P0到P1和P2到P3之間的曲線,我們需要在這幾個曲線段外再取兩個點,我的做法是取P1P0和P2P3兩個向量計算出首位兩個點。

3.樣條曲線類

下面的程式碼段是一個完成的樣條曲線類,可以直接使用,後面會貼上這個類的使用方式

// ==========================================
// 描述: 
// 作者: HAK
// 時間: 2018-11-28 11:31:34
// 版本: V 1.0
// ==========================================
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 樣條型別
/// </summary>
public enum SplineMode
{
    Hermite,               // 埃爾米特樣條
    Catmull_Rom,           // Catmull_Rom 建議選擇
    CentripetalCatmull_Rom,// 向心Catmull_Rom
}

/// <summary>
/// 樣條曲線
/// </summary>
public class SplineCurve
{
    /// <summary>
    /// 曲線起始節點
    /// </summary>
    private Node startNode;
    /// <summary>
    /// 曲線終結點
    /// </summary>
    private Node endNode;
    /// <summary>
    /// 節點集合
    /// </summary>
    private List<Node> nodeList;
    /// <summary>
    /// 節點法線集合
    /// </summary>
    private List<Vector3> tangentsList;
    /// <summary>
    /// 曲線段集合
    /// </summary>
    public List<CurveSegement> segmentList { get; private set; }
    /// <summary>
    /// 曲線構造型別
    /// </summary>
    public SplineMode mode { get; private set; }

    public SplineCurve(SplineMode _mode = SplineMode.Catmull_Rom)
    {
        nodeList = new List<Node>();
        tangentsList = new List<Vector3>();
        segmentList = new List<CurveSegement>();
        mode = _mode;
    }

    /// <summary>
    /// 新增首尾控制點
    /// </summary>
    public void AddCatmull_RomControl()
    {
        if(mode != SplineMode.Catmull_Rom)
        {
            Debug.Log("不是Catmull樣條");
            return;
        }
        if(nodeList.Count < 2)
        {
            Debug.Log("Catmull_Rom樣條取點要大於等於2");
            return;
        }
        Node node = new Node(startNode.pos + (nodeList[0].pos - nodeList[1].pos), null, nodeList[0]);
        nodeList.Insert(0, node);
        node = new Node(endNode.pos + (endNode.pos - nodeList[nodeList.Count - 2].pos), nodeList[nodeList.Count - 1]);
        nodeList.Add(node);
    }

    /// <summary>
    /// 新增節點
    /// </summary>
    /// <param name="newNode"></param>
    public void AddNode(Vector3 pos, float c)
    {
        Node node;
        if(nodeList.Count < 1)
        {
            node = new Node(pos);
        }
        else
        {
            node = new Node(pos, nodeList[nodeList.Count - 1]);
        }
        nodeList.Add(node);

        
        if(nodeList.Count > 1)
        {
            CurveSegement a = new CurveSegement(endNode, node,this);
            a.c = c;
            segmentList.Add(a);
            CaculateTangents(segmentList.Count - 1);               // 計算新加入的曲線段起始切線
        }
        else // 加入第一個節點
        {
            startNode = node;
        }
        endNode = node;
    }

    /// <summary>
    /// 獲取點
    /// </summary>
    /// <param name="index"></param>
    /// <param name="t"></param>
    public void GetPoint(int index, float t)
    {
        segmentList[index].GetPoint(t);
    }

    /// <summary>
    /// 獲取切線
    /// </summary>
    /// <param name="index"></param>
    /// <param name="t"></param>
    public void GetTangents(int index, float t)
    {
        segmentList[index].GetTangents(t);
    }

    /// <summary>
    /// 計算曲線段首尾切線
    /// </summary>
    /// <param name="index"></param>
    private void CaculateTangents(int index)
    {
        CurveSegement segement = segmentList[index];

        if(index == 0)
        {
            segement.startTangents = segement.endNode.pos - segement.endNode.pos;
            segement.endTangents = segement.endNode.pos - segement.startNode.pos;
            return;
        }

        CurveSegement preSegement = segmentList[index - 1];

        segement.startTangents = 0.5f * (1 - segement.c) * (segement.endNode.pos - preSegement.endNode.pos);
        segement.endTangents = segement.endNode.pos - segement.startNode.pos;
        preSegement.endTangents = segement.startTangents;

    }
}

/// <summary>
/// 曲線段
/// </summary>
public class CurveSegement
{
    /// <summary>
    /// 所屬曲線
    /// </summary>
    public SplineCurve rootCurve;

    /// <summary>
    /// 曲線段起始位置
    /// </summary>
    public Node startNode { get; private set; }
    /// <summary>
    /// 曲線段末尾位置
    /// </summary>
    public Node endNode { get; private set; }

    public Vector3 startTangents;
    public Vector3 endTangents;

    /// <summary>
    /// 張力系數
    /// </summary>
    public float c { get;  set; }

    public CurveSegement(Node _startNode,Node _endNode,SplineCurve _rootCurve)
    {
        startNode = _startNode;
        endNode = _endNode;
        rootCurve = _rootCurve;
        c = -5f;
    }

    /// <summary>
    /// 獲取點
    /// </summary>
    /// <param name="t"></param>
    /// <returns></returns>
    public Vector3 GetPoint(float t)
    {
        Vector3 x = Vector3.zero;
        switch (rootCurve.mode)
        {
            case SplineMode.Hermite:
                x = (2 * t * t * t - 3 * t * t + 1) * startNode.pos;
                x += (-2 * t * t * t + 3 * t * t) * endNode.pos;
                x += (t * t * t - 2 * t * t + t) * startTangents;
                x += (t * t * t - t * t) * endTangents;
                break;
            case SplineMode.Catmull_Rom:
                x += startNode.preNode.pos * (-0.5f * t * t * t + t * t - 0.5f * t);
                x += startNode.pos * (1.5f * t * t * t - 2.5f * t * t + 1.0f);
                x += endNode.pos * (-1.5f * t * t * t + 2.0f * t * t + 0.5f * t);
                x += endNode.nextNode.pos * (0.5f * t * t * t - 0.5f * t * t);
                break;
            case SplineMode.CentripetalCatmull_Rom:
                break;
            default:
                break;
        }

        return x;

    }

    /// <summary>
    /// 獲取切線
    /// </summary>
    /// <param name="t"></param>
    /// <returns></returns>
    public Vector3 GetTangents(float t)
    {
        Vector3 tangents = Vector3.zero;
        switch (rootCurve.mode)
        {
            case SplineMode.Hermite:
                tangents = (6 * t * t - 6 * t) * startNode.pos;
                tangents += (-6 * t * t + 6 * t) * endNode.pos;
                tangents += (3 * t * t - 4 * t + 1) * startTangents;
                tangents += (3 * t * t - 2 * t) * endTangents;
                break;
            case SplineMode.Catmull_Rom:
                tangents = startNode.preNode.pos * (-1.5f * t * t + 2 * t - 0.5f);
                tangents += startNode.pos * (3.0f * t * t - 5.0f * t);
                tangents += endNode.pos * (-3.0f * t * t + 4.0f * t + 0.5f);
                tangents += endNode.nextNode.pos * (1.5f * t * t - 1.0f * t);
                break;
            case SplineMode.CentripetalCatmull_Rom:
                break;
            default:
                break;
        }
        
        return tangents;
    }
}

/// <summary>
/// 曲線節點
/// </summary>
public class Node
{
    /// <summary>
    /// 節點位置
    /// </summary>
    public Vector3 pos;
    /// <summary>
    /// 前連線節點
    /// </summary>
    public Node preNode;
    /// <summary>
    /// 後連線節點
    /// </summary>
    public Node nextNode;

    public Node(Vector3 _pos)
    {
        pos = _pos;
    }

    public Node(Vector3 _pos, Node _preNode, Node _nextNode = null)
    {
        pos = _pos;
        if(_preNode != null)
        {
            preNode = _preNode;
            _preNode.nextNode = this;
        }
        if(_nextNode != null)
        {
            nextNode = _nextNode;
            _nextNode.preNode = this;
        }
    }
}

 

這個是我用 Catmull-Rom計算出來的點集結合Mesh繪圖繪製出來的一條道路,擬合效果還算不錯

最後貼上樣條曲線類的使用,第一步建立,第二部加點,第三步新增首尾控制點,最後得到的path就是點集,大家可以遍歷點集畫線或者建立cube來觀察曲線。

SplineCurve curve = new SplineCurve();  //新建曲線,預設為Catmull-Rom樣條

 curve.AddNode(point1);  // 加入至少兩個關鍵點 

 curve.AddNode(point2);  // 程式碼裡的AddNode還有以引數c,可以去掉,這是用來測試的

 outCurve.AddNode(point3);

。。。

 curve.AddCatmull_RomControl();  // 加入首位兩個控制點


List<Vector3> path = new List<Vector3>();
 for (int i = 0; i < outCurve.segmentList.Count; i++)
{
    float add = 1f / 20;  // 表示兩個關鍵點之間取20個點,可根據需要設定
    for (float j = 0; j < 1; j += add)
    {
        Vector3 point = centerCurve.segmentList[i].GetPoint(j);
        path.Add(point);
    }
}

曲線擬合本身就是一個公式,簡單點寫可以之間寫成一個方法傳進一組關鍵點返回一組點集,但是這樣不利於擴充套件,也不利於對整個曲線上的每條曲線段進行單個控制。