一、前言

点关注不迷路,持续输出Unity干货文章。
嗨,大家好,我是新发。
有同学私信我,问我能否出一个三阶贝塞尔曲线的教程。
在这里插入图片描述
嘛,今天就来讲讲贝塞尔曲线吧。

二、最终效果

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、运行测试

最终运行测试效果如下:
在这里插入图片描述

七、结束语

现在,贝塞尔曲线,你学会了吗?

喜欢Unity的同学,不要忘记点击关注,如果有什么Unity相关的技术难题,也欢迎留言或私信~

更多推荐