点关注不迷路,持续输出Unity
干货文章。
嗨,大家好,我是新发。
有同学私信我,问我能否出一个三阶贝塞尔曲线的教程。
嘛,今天就来讲讲贝塞尔曲线吧。
最终运行效果如下:
本文Demo
工程已上传到CodeChina
,感兴趣的同学可自行下载学习。
地址:https://codechina.csdn.net/linxinfa/unitybeziercurvedrawdemo
注:我使用的Unity
版本:Unity 2020.1.14f1c1 (64-bit)
。
贝塞尔曲线(Bezier curve
),又称 贝兹曲线 或 贝济埃曲线 ,由法国工程师皮埃尔·贝塞尔(Pierre Bézier
)所广泛发表,当时主要用于汽车主体设计。现主要应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,比如PhotoShop
中的钢笔工具。
PhotoShop
中的钢笔工具是一个三阶的贝塞尔曲线。
我们说的三阶贝塞尔曲线,三阶是什么意思?
两种理解:
1、贝塞尔曲线,它的背后是一个数学函数,N
阶可以理解为N
次方的意思,我们也可以把三阶贝塞尔曲线叫做三次贝塞尔曲线。
2、二阶贝塞尔曲线就是在一阶贝塞尔曲线的基础上再求一次一阶贝塞尔曲线;三阶贝塞尔曲线就是在二阶贝塞尔曲线的基础上再求一次一阶贝塞尔曲线;以此类推。
我觉得第二种理解更准确一点。
我们先来看看 一阶贝塞尔曲线 的公式:
给定点P1 P2,函数推导式如下:
一阶贝塞尔曲线公式:B(t) = P1 + (P2 − P1)t = P1(1−t)+ P2t, t∈[0,1]
动态效果如下:
注:应该会有同学想问下图这个动态效果是用什么软件做的,是使用js写的,感兴趣的同学可以跳到文章第四节:贝塞尔曲线本地实验
从动态效果看,我们可以看出,一阶贝塞尔曲线其实就是一条线性的线段。
二阶贝塞尔曲线的路径由给定点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,
这正是我们上面一开始列的二阶贝塞尔曲线的公式。
根据一阶、二阶的贝塞尔曲线的原理,相信大家已经知道三阶贝塞尔曲线的推导了吧。
三阶贝塞尔曲线公式: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]
更高阶的贝塞尔曲线的公式推导就不一一写了,我们来玩个猛的,50阶贝塞尔曲线:
上文中我演示的贝塞尔曲线动态效果,是通过GitHub
的一个开源项目进行演示的。
GitHub
源地址:https://github.com/Aaaaaaaty/bezierMaker.js
我在它的基础上做了一些改进,上传到了CodeChina
,感兴趣的可以下载我的版本:
CodeChina
地址:https://codechina.csdn.net/linxinfa/beziermaker
下载下来后,直接用浏览器打开bezierMaker.html
即可。
如下:
感受到了数学之美了吗?感兴趣的同学可以自己玩一下。我发几个可以在线实验贝塞尔曲线的网址给大家吧。
地址:https://csdjk.github.io/bezierPathCreater.github.io/
地址:http://wx.karlew.com/canvas/bezier/
上面啰嗦了这么多,我终于要讲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
using UnityEngine;
///
/// 贝塞尔曲线
///
[ExecuteInEditMode]
public class BezierCurve : MonoBehaviour
{
///
/// 控制点(包括起始点和终止点)
///
[SerializeField]
Transform[] points;
///
/// 精确度
///
[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);
}
}
}
///
/// 贝塞尔时间公式(二阶、三阶)
///
/// 时间参数,范围0~1
///
public Vector3 formula(float t)
{
switch(points.Length)
{
case 3: return quardaticBezier(t);
case 4: return cubicBezier(t);
}
return Vector3.zero;
}
///
/// 一阶贝塞尔
///
/// 时间参数,范围0~1
///
public Vector3 lineBezier(float t)
{
Vector3 a = points[0].position;
Vector3 b = points[1].position;
return a + (b - a) * t;
}
///
/// 二阶贝塞尔
///
/// 时间参数,范围0~1
///
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;
}
///
/// 三阶贝塞尔
///
/// 时间参数,范围0~1
///
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;
}
}
曲线的绘制,我使用了LineRenderer
组件,为了让效果看起来好看一点,我调了个彩虹渐变色,我把宽度调成两边细中间粗,这样看起来更加立体。
效果如下:
我分别制作了二阶贝塞尔曲线和三阶贝塞尔曲线的预设。
二阶贝塞尔曲线预设:
预设上挂的组件如下:
其中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;
///
/// LineRenderer控制器
///
[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);
}
}
}
控制点我用的是球体,我使用了射线检测来判断鼠标是否点中了控制点。
示例:
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;
///
/// 控制点
///
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;
}
}
}
简单搭建一下场景,制作一下界面
写个界面逻辑脚本UICtrler.cs
,
UICtrler.cs
挂在Canvas
节点上,并赋值对应成员。
UICtrler.cs
代码如下:
// UICtrler.cs
using UnityEngine;
using UnityEngine.UI;
public class UICtrler : MonoBehaviour
{
///
/// 二阶贝塞尔曲线
///
public GameObject bezierCurve2;
///
/// 三阶贝塞尔曲线
///
public GameObject bezierCurve3;
public Toggle toggleBezier3;
void Start()
{
toggleBezier3.onValueChanged.AddListener((v) =>
{
bezierCurve3.SetActive(v);
bezierCurve2.SetActive(!v);
});
// 默认显示三阶贝塞尔曲线
bezierCurve3.SetActive(true);
bezierCurve2.SetActive(false);
}
}
最终运行测试效果如下:
现在,贝塞尔曲线,你学会了吗?
喜欢Unity
的同学,不要忘记点击关注,如果有什么Unity
相关的技术难题,也欢迎留言或私信~