天天看點

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

1.三次Hermite樣條

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

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類
Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

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

        三次Hermite內插補點實際上是貝塞爾曲線的轉型,它将兩個外控制點轉成了兩個切線,維基百科對Cubic Hermite spline解釋比較清楚,貼上連結以供參考https://en.wikipedia.org/wiki/Cubic_Hermite_spline。下為三次Hermite 樣條曲線的公式

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類
Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

轉存失敗重新上傳取消

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

,t∈[0,1]

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

2.Catmull-Rom Spline

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

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類
Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

轉存失敗重新上傳取消

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

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

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

為了拟合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繪圖繪制出來的一條道路,拟合效果還算不錯

Unity 曲線插值(Hermite插值和Catmull_Rom插值)1.三次Hermite樣條2.Catmull-Rom Spline3.樣條曲線類

最後貼上樣條曲線類的使用,第一步建立,第二部加點,第三步添加首尾控制點,最後得到的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);
    }
}
           

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