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 样条曲线的公式

c67f2482bc328d3c50769b8d860b9488c936af63uploading.4e448015.gif转存失败重新上传取消\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

         14cab67f-70f8-4c3e-873a-b028bf64ef4d.pnguploading.4e448015.gif转存失败重新上传取消

         注意,上图的四个点只能模拟出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);
    }
}

曲线拟合本身就是一个公式,简单点写可以之间写成一个方法传进一组关键点返回一组点集,但是这样不利于扩展,也不利于对整个曲线上的每条曲线段进行单个控制。

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值