贝塞尔曲线(Bezier Curve
),又称贝兹曲线或贝济埃曲线,是计算机图形学中相当重要的参数曲线,在我们常用的软件如Photo Shop
中就有贝塞尔曲线工具,本文简单介绍贝塞尔曲线在Unity中的实现与应用。
给顶点P0、P1,只是一条两点之间的直线,公式如下:
B(t) = P0 + (P1 - P0) t = (1 - t) P0 + t P1, t ∈ [0, 1]
等同于线性插值,代码实现如下:
///
/// 一阶贝塞尔曲线
///
/// 起点
/// 终点
/// [0,1]
///
public static Vector3 Bezier1(Vector3 p0, Vector3 p1, float t)
{
return (1 - t) * p0 + t * p1;
}
路径由给定点P0、P1、P2的函数计算,公式如下:
B(t) = (1 - t)2 P0 + 2t (1 - t) P1 + t2P2, t ∈[0, 1]
代码实现如下:
///
/// 二阶贝塞尔曲线
///
/// 起点
/// 控制点
/// 终点
/// [0,1]
///
public static Vector3 Bezier2(Vector3 p0, Vector3 p1, Vector3 p2, float t)
{
Vector3 p0p1 = (1 - t) * p0 + t * p1;
Vector3 p1p2 = (1 - t) * p1 + t * p2;
return (1 - t) * p0p1 + t * p1p2;
}
P0、P1、P2、P3四个点在平面或三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3,一般不会经过P1、P2,这两个点只是提供方向信息,可以将P1、P2理解为控制点。P0和P1之间的间距,决定了曲线在转而趋近P3之前,走向P2的长度有多长,公式如下:
B(t) = P0(1 - t)3 + 3P1t(1 - t)2 + 3P2t2(1 - t) + P3t3, t ∈ [0, 1]
代码实现如下:
///
/// 三阶贝塞尔曲线
///
/// 起点
/// 控制点1
/// 控制点2
/// 终点
/// [0,1]
///
public static Vector3 Bezier3(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
Vector3 p0p1 = (1 - t) * p0 + t * p1;
Vector3 p1p2 = (1 - t) * p1 + t * p2;
Vector3 p2p3 = (1 - t) * p2 + t * p3;
Vector3 p0p1p2 = (1 - t) * p0p1 + t * p1p2;
Vector3 p1p2p3 = (1 - t) * p1p2 + t * p2p3;
return (1 - t) * p0p1p2 + t * p1p2p3;
}
使用Gizmos
绘制Bezier Curve
,通过图形理解贝塞尔曲线:
P0为起点,P1为终点,t从0到1时,在贝塞尔曲线上对应的点为Pt,可以将t为理解为动画播放中的normalized time
代码如下:
using UnityEngine;
using SK.Framework;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class Example : MonoBehaviour
{
private float t;
private void Update()
{
if (t < 1f)
{
t += Time.deltaTime * .2f;
t = Mathf.Clamp01(t);
}
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Gizmos.color = Color.grey;
Vector3 p0 = Vector3.left * 5f;
Vector3 p1 = Vector3.right * 5f;
Gizmos.DrawLine(p0, p1);
Handles.Label(p0, "P0");
Handles.Label(p1, "P1");
Handles.SphereHandleCap(0, p0, Quaternion.identity, .1f, EventType.Repaint);
Handles.SphereHandleCap(0, p1, Quaternion.identity, .1f, EventType.Repaint);
Vector3 pt = BezierCurveUtility.Bezier1(p0, p1, t);
Gizmos.color = Color.red;
Gizmos.DrawLine(p0, pt);
Handles.Label(pt, string.Format("Pt (t = {0})", t));
Handles.SphereHandleCap(0, pt, Quaternion.identity, .1f, EventType.Repaint);
}
#endif
}
P0为起点,P1为控制点,P2为终点,t从0到1时,在贝塞尔曲线上对应的点为Pt
代码如下:
using UnityEngine;
using SK.Framework;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class Example : MonoBehaviour
{
private float t;
private void Update()
{
if (t < 1f)
{
t += Time.deltaTime * .2f;
t = Mathf.Clamp01(t);
}
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Gizmos.color = Color.grey;
Vector3 p0 = Vector3.left * 5f;
Vector3 p1 = Vector3.left * 2f + Vector3.forward * 2f;
Vector3 p2 = Vector3.right * 5f;
Gizmos.DrawLine(p0, p1);
Gizmos.DrawLine(p2, p1);
Handles.Label(p0, "P0");
Handles.Label(p1, "P1");
Handles.Label(p2, "P2");
Handles.SphereHandleCap(0, p0, Quaternion.identity, .1f, EventType.Repaint);
Handles.SphereHandleCap(0, p1, Quaternion.identity, .1f, EventType.Repaint);
Handles.SphereHandleCap(0, p2, Quaternion.identity, .1f, EventType.Repaint);
Gizmos.color = Color.green;
for (int i = 0; i < 100; i++)
{
Vector3 curr = BezierCurveUtility.Bezier2(p0, p1, p2, i / 100f);
Vector3 next = BezierCurveUtility.Bezier2(p0, p1, p2, (i + 1) / 100f);
Gizmos.color = t > (i / 100f) ? Color.red : Color.green;
Gizmos.DrawLine(curr, next);
}
Vector3 pt = BezierCurveUtility.Bezier2(p0, p1, p2, t);
Handles.Label(pt, string.Format("Pt (t = {0})", t));
Handles.SphereHandleCap(0, pt, Quaternion.identity, .1f, EventType.Repaint);
}
#endif
}
P0为起点,P1为第一个控制点,P2为第二个控制点,P3为终点,t从0到1时,在贝塞尔曲线上对应的点为Pt
代码如下:
using UnityEngine;
using SK.Framework;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class Example : MonoBehaviour
{
private float t;
private void Update()
{
if (t < 1f)
{
t += Time.deltaTime * .2f;
t = Mathf.Clamp01(t);
}
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
Gizmos.color = Color.grey;
Vector3 p0 = Vector3.left * 5f;
Vector3 p1 = Vector3.left * 2f + Vector3.forward * 2f;
Vector3 p2 = Vector3.right * 3f + Vector3.back * 4f;
Vector3 p3 = Vector3.right * 5f;
Gizmos.DrawLine(p0, p1);
Gizmos.DrawLine(p1, p2);
Gizmos.DrawLine(p2, p3);
Handles.Label(p0, "P0");
Handles.Label(p1, "P1");
Handles.Label(p2, "P2");
Handles.Label(p3, "P3");
Handles.SphereHandleCap(0, p0, Quaternion.identity, .1f, EventType.Repaint);
Handles.SphereHandleCap(0, p1, Quaternion.identity, .1f, EventType.Repaint);
Handles.SphereHandleCap(0, p2, Quaternion.identity, .1f, EventType.Repaint);
Handles.SphereHandleCap(0, p3, Quaternion.identity, .1f, EventType.Repaint);
Gizmos.color = Color.green;
for (int i = 0; i < 100; i++)
{
Vector3 curr = BezierCurveUtility.Bezier3(p0, p1, p2, p3, i / 100f);
Vector3 next = BezierCurveUtility.Bezier3(p0, p1, p2, p3, (i + 1) / 100f);
Gizmos.color = t > (i / 100f) ? Color.red : Color.green;
Gizmos.DrawLine(curr, next);
}
Vector3 pt = BezierCurveUtility.Bezier3(p0, p1, p2, p3, t);
Handles.Label(pt, string.Format("Pt (t = {0})", t));
Handles.SphereHandleCap(0, pt, Quaternion.identity, .1f, EventType.Repaint);
}
#endif
}
常见的如道路编辑、河流编辑功能都可以通过贝塞尔曲线实现:
本文以一个简单的路径编辑为例,通过使用三阶贝塞尔曲线实现路径的编辑:
segments
:贝塞尔曲线的段数,值越大曲线精度越高;loop
:是否循环(首尾相连);points
:点集合(结构体中包含坐标点和控制点);using System;
using UnityEngine;
using System.Collections.Generic;
namespace SK.Framework
{
///
/// 贝塞尔曲线
///
[Serializable]
public class BezierCurve
{
///
/// 段数
///
[Range(1, 100)] public int segments = 10;
///
/// 是否循环
///
public bool loop;
///
/// 点集合
///
public List<BezierCurvePoint> points = new List<BezierCurvePoint>(2)
{
new BezierCurvePoint() { position = Vector3.back * 5f, tangent = Vector3.back * 5f + Vector3.left * 3f },
new BezierCurvePoint() { position = Vector3.forward * 5f, tangent = Vector3.forward * 5f + Vector3.right * 3f }
};
///
/// 根据归一化位置值获取对应的贝塞尔曲线上的点
///
/// 归一化位置值 [0,1]
///
public Vector3 EvaluatePosition(float t)
{
Vector3 retVal = Vector3.zero;
if (points.Count > 0)
{
float max = points.Count - 1 < 1 ? 0 : (loop ? points.Count : points.Count - 1);
float standardized = (loop && max > 0) ? ((t %= max) + (t < 0 ? max : 0)) : Mathf.Clamp(t, 0, max);
int rounded = Mathf.RoundToInt(standardized);
int i1, i2;
if (Mathf.Abs(standardized - rounded) < Mathf.Epsilon)
i1 = i2 = (rounded == points.Count) ? 0 : rounded;
else
{
i1 = Mathf.FloorToInt(standardized);
if (i1 >= points.Count)
{
standardized -= max;
i1 = 0;
}
i2 = Mathf.CeilToInt(standardized);
i2 = i2 >= points.Count ? 0 : i2;
}
retVal = i1 == i2 ? points[i1].position : BezierCurveUtility.Bezier3(points[i1].position,
points[i1].position + points[i1].tangent, points[i2].position
- points[i2].tangent, points[i2].position, standardized - i1);
}
return retVal;
}
}
}
using System;
using UnityEngine;
namespace SK.Framework
{
[Serializable]
public struct BezierCurvePoint
{
///
/// 坐标点
///
public Vector3 position;
///
/// 控制点 与坐标点形成切线
///
public Vector3 tangent;
}
}
using UnityEngine;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SK.Framework
{
///
/// 贝塞尔曲线路径
///
public class SimpleBezierCurvePath : MonoBehaviour
{
[SerializeField] private BezierCurve curve;
public bool Loop { get { return curve.loop; } }
public List<BezierCurvePoint> Points { get { return curve.points; } }
///
/// 根据归一化位置值获取对应的贝塞尔曲线上的点
///
/// 归一化位置值 [0,1]
///
public Vector3 EvaluatePosition(float t)
{
return curve.EvaluatePosition(t);
}
#if UNITY_EDITOR
///
/// 路径颜色(Gizmos)
///
public Color pathColor = Color.green;
private void OnDrawGizmos()
{
if (curve.points.Count == 0) return;
//缓存颜色
Color cacheColor = Gizmos.color;
//路径绘制颜色
Gizmos.color = pathColor;
//步长
float step = 1f / curve.segments;
//缓存上个坐标点
Vector3 lastPos = transform.TransformPoint(curve.EvaluatePosition(0f));
float end = (curve.points.Count - 1 < 1 ? 0 : (curve.loop ? curve.points.Count : curve.points.Count - 1)) + step * .5f;
for (float t = step; t <= end; t += step)
{
//计算位置
Vector3 p = transform.TransformPoint(curve.EvaluatePosition(t));
//绘制曲线
Gizmos.DrawLine(lastPos, p);
//记录
lastPos = p;
}
//恢复颜色
Gizmos.color = cacheColor;
}
#endif
}
#if UNITY_EDITOR
[CustomEditor(typeof(SimpleBezierCurvePath))]
public class SimpleBezierCurvePathEditor : Editor
{
private SimpleBezierCurvePath path;
private const float sphereHandleCapSize = .2f;
private void OnEnable()
{
path = target as SimpleBezierCurvePath;
}
private void OnSceneGUI()
{
//路径点集合为空
if (path.Points == null || path.Points.Count == 0) return;
//当前选中工具非移动工具
if (Tools.current != Tool.Move) return;
//颜色缓存
Color cacheColor = Handles.color;
Handles.color = Color.yellow;
//遍历路径点集合
for (int i = 0; i < path.Points.Count; i++)
{
DrawPositionHandle(i);
DrawTangentHandle(i);
BezierCurvePoint point = path.Points[i];
//局部转全局坐标 路径点、控制点
Vector3 position = path.transform.TransformPoint(point.position);
Vector3 controlPoint = path.transform.TransformPoint(point.tangent);
//绘制切线
Handles.DrawDottedLine(position, controlPoint + position, 1f);
}
//恢复颜色
Handles.color = cacheColor;
}
//路径点操作柄绘制
private void DrawPositionHandle(int index)
{
BezierCurvePoint point = path.Points[index];
//局部转全局坐标
Vector3 position = path.transform.TransformPoint(point.position);
//操作柄的旋转类型
Quaternion rotation = Tools.pivotRotation == PivotRotation.Local
? path.transform.rotation : Quaternion.identity;
//操作柄的大小
float size = HandleUtility.GetHandleSize(position) * sphereHandleCapSize;
//在该路径点绘制一个球形
Handles.color = Color.white;
Handles.SphereHandleCap(0, position, rotation, size, EventType.Repaint);
Handles.Label(position, string.Format("Point{0}", index));
//检测变更
EditorGUI.BeginChangeCheck();
//坐标操作柄
position = Handles.PositionHandle(position, rotation);
//变更检测结束 如果发生变更 更新路径点
if (EditorGUI.EndChangeCheck())
{
//记录操作
Undo.RecordObject(path, "Position Changed");
//全局转局部坐标
point.position = path.transform.InverseTransformPoint(position);
//更新路径点
path.Points[index] = point;
}
}
//控制点操作柄绘制
private void DrawTangentHandle(int index)
{
BezierCurvePoint point = path.Points[index];
//局部转全局坐标
Vector3 cp = path.transform.TransformPoint(point.position + point.tangent);
//操作柄的旋转类型
Quaternion rotation = Tools.pivotRotation == PivotRotation.Local
? path.transform.rotation : Quaternion.identity;
//操作柄的大小
float size = HandleUtility.GetHandleSize(cp) * sphereHandleCapSize;
//在该控制点绘制一个球形
Handles.color = Color.yellow;
Handles.SphereHandleCap(0, cp, rotation, size, EventType.Repaint);
//检测变更
EditorGUI.BeginChangeCheck();
//坐标操作柄
cp = Handles.PositionHandle(cp, rotation);
//变更检测结束 如果发生变更 更新路径点
if (EditorGUI.EndChangeCheck())
{
//记录操作
Undo.RecordObject(path, "Control Point Changed");
//全局转局部坐标
point.tangent = path.transform.InverseTransformPoint(cp) - point.position;
//更新路径点
path.Points[index] = point;
}
}
}
#endif
}
path
:贝塞尔曲线路径;speed
:移动速度;update Mode
:更新方式(FixedUpdate、Update、LateUpdate)using UnityEngine;
namespace SK.Framework
{
public class SimpleBezierCurvePathAlonger : MonoBehaviour
{
public enum UpdateMode
{
FixedUpdate,
Update,
LateUpdate,
}
[SerializeField] private SimpleBezierCurvePath path;
[SerializeField] private float speed = .1f;
[SerializeField] private UpdateMode updateMode = UpdateMode.Update;
private float normalized = 0f;
private Vector3 lastPosition;
private void FixedUpdate()
{
if (updateMode == UpdateMode.FixedUpdate && path != null)
MoveAlongPath();
}
private void Update()
{
if (updateMode == UpdateMode.Update && path != null)
MoveAlongPath();
}
private void LateUpdate()
{
if (updateMode == UpdateMode.LateUpdate && path != null)
MoveAlongPath();
}
private void MoveAlongPath()
{
float t = normalized + speed * Time.deltaTime;
float max = path.Points.Count - 1 < 1 ? 0 : (path.Loop ? path.Points.Count : path.Points.Count - 1);
normalized = (path.Loop && max > 0) ? ((t %= max) + (t < 0 ? max : 0)) : Mathf.Clamp(t, 0, max);
transform.position = path.EvaluatePosition(normalized);
Vector3 forward = transform.position - lastPosition;
transform.forward = forward != Vector3.zero ? forward : transform.forward;
lastPosition = transform.position;
}
}
}
源码已上传至SKFramework
框架Package Manager
中:
参考链接: