簡介
貝塞爾曲線(Bezier Curve
),又稱貝茲曲線或貝濟(jì)埃曲線,是計(jì)算機(jī)圖形學(xué)中相當(dāng)重要的參數(shù)曲線,在我們常用的軟件如Photo Shop
中就有貝塞爾曲線工具,本文簡單介紹貝塞爾曲線在Unity中的實(shí)現(xiàn)與應(yīng)用。
一階貝塞爾曲線
給頂點(diǎn)P0、P1,只是一條兩點(diǎn)之間的直線,公式如下:
B(t) = P0 + (P1 - P0) t = (1 - t) P0 + t P1, t ∈ [0, 1]
等同于線性插值,代碼實(shí)現(xiàn)如下:
/// <summary>
/// 一階貝塞爾曲線
/// </summary>
/// <param name="p0">起點(diǎn)</param>
/// <param name="p1">終點(diǎn)</param>
/// <param name="t">[0,1]</param>
/// <returns></returns>
public static Vector3 Bezier1(Vector3 p0, Vector3 p1, float t)
{
return (1 - t) * p0 + t * p1;
}
二階貝塞爾曲線
路徑由給定點(diǎn)P0、P1、P2的函數(shù)計(jì)算,公式如下:
B(t) = (1 - t)2 P0 + 2t (1 - t) P1 + t2P2, t ∈[0, 1]
代碼實(shí)現(xiàn)如下:
/// <summary>
/// 二階貝塞爾曲線
/// </summary>
/// <param name="p0">起點(diǎn)</param>
/// <param name="p1">控制點(diǎn)</param>
/// <param name="p2">終點(diǎn)</param>
/// <param name="t">[0,1]</param>
/// <returns></returns>
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四個(gè)點(diǎn)在平面或三維空間中定義了三次方貝塞爾曲線。曲線起始于P0走向P1,并從P2的方向來到P3,一般不會(huì)經(jīng)過P1、P2,這兩個(gè)點(diǎn)只是提供方向信息,可以將P1、P2理解為控制點(diǎn)。P0和P1之間的間距,決定了曲線在轉(zhuǎn)而趨近P3之前,走向P2的長度有多長,公式如下:
B(t) = P0(1 - t)3 + 3P1t(1 - t)2 + 3P2t2(1 - t) + P3t3, t ∈ [0, 1]
代碼實(shí)現(xiàn)如下:
/// <summary>
/// 三階貝塞爾曲線
/// </summary>
/// <param name="p0">起點(diǎn)</param>
/// <param name="p1">控制點(diǎn)1</param>
/// <param name="p2">控制點(diǎn)2</param>
/// <param name="p3">終點(diǎn)</param>
/// <param name="t">[0,1]</param>
/// <returns></returns>
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;
}
圖形理解 Bezier Curve
使用Gizmos
繪制Bezier Curve
,通過圖形理解貝塞爾曲線:
一階貝塞爾曲線
P0為起點(diǎn),P1為終點(diǎn),t從0到1時(shí),在貝塞爾曲線上對應(yīng)的點(diǎn)為Pt,可以將t為理解為動(dòng)畫播放中的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為起點(diǎn),P1為控制點(diǎn),P2為終點(diǎn),t從0到1時(shí),在貝塞爾曲線上對應(yīng)的點(diǎn)為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為起點(diǎn),P1為第一個(gè)控制點(diǎn),P2為第二個(gè)控制點(diǎn),P3為終點(diǎn),t從0到1時(shí),在貝塞爾曲線上對應(yīng)的點(diǎn)為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
}
應(yīng)用
常見的如道路編輯、河流編輯功能都可以通過貝塞爾曲線實(shí)現(xiàn):
本文以一個(gè)簡單的路徑編輯為例,通過使用三階貝塞爾曲線實(shí)現(xiàn)路徑的編輯:
Bezier Curve
-
segments
:貝塞爾曲線的段數(shù),值越大曲線精度越高; -
loop
:是否循環(huán)(首尾相連); -
points
:點(diǎn)集合(結(jié)構(gòu)體中包含坐標(biāo)點(diǎn)和控制點(diǎn));
using System;
using UnityEngine;
using System.Collections.Generic;
namespace SK.Framework
{
/// <summary>
/// 貝塞爾曲線
/// </summary>
[Serializable]
public class BezierCurve
{
/// <summary>
/// 段數(shù)
/// </summary>
[Range(1, 100)] public int segments = 10;
/// <summary>
/// 是否循環(huán)
/// </summary>
public bool loop;
/// <summary>
/// 點(diǎn)集合
/// </summary>
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 }
};
/// <summary>
/// 根據(jù)歸一化位置值獲取對應(yīng)的貝塞爾曲線上的點(diǎn)
/// </summary>
/// <param name="t">歸一化位置值 [0,1]</param>
/// <returns></returns>
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
{
/// <summary>
/// 坐標(biāo)點(diǎn)
/// </summary>
public Vector3 position;
/// <summary>
/// 控制點(diǎn) 與坐標(biāo)點(diǎn)形成切線
/// </summary>
public Vector3 tangent;
}
}
SimpleBezierCurvePath
using UnityEngine;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SK.Framework
{
/// <summary>
/// 貝塞爾曲線路徑
/// </summary>
public class SimpleBezierCurvePath : MonoBehaviour
{
[SerializeField] private BezierCurve curve;
public bool Loop { get { return curve.loop; } }
public List<BezierCurvePoint> Points { get { return curve.points; } }
/// <summary>
/// 根據(jù)歸一化位置值獲取對應(yīng)的貝塞爾曲線上的點(diǎn)
/// </summary>
/// <param name="t">歸一化位置值 [0,1]</param>
/// <returns></returns>
public Vector3 EvaluatePosition(float t)
{
return curve.EvaluatePosition(t);
}
#if UNITY_EDITOR
/// <summary>
/// 路徑顏色(Gizmos)
/// </summary>
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;
//緩存上個(gè)坐標(biāo)點(diǎn)
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)
{
//計(jì)算位置
Vector3 p = transform.TransformPoint(curve.EvaluatePosition(t));
//繪制曲線
Gizmos.DrawLine(lastPos, p);
//記錄
lastPos = p;
}
//恢復(fù)顏色
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()
{
//路徑點(diǎn)集合為空
if (path.Points == null || path.Points.Count == 0) return;
//當(dāng)前選中工具非移動(dòng)工具
if (Tools.current != Tool.Move) return;
//顏色緩存
Color cacheColor = Handles.color;
Handles.color = Color.yellow;
//遍歷路徑點(diǎn)集合
for (int i = 0; i < path.Points.Count; i++)
{
DrawPositionHandle(i);
DrawTangentHandle(i);
BezierCurvePoint point = path.Points[i];
//局部轉(zhuǎn)全局坐標(biāo) 路徑點(diǎn)、控制點(diǎn)
Vector3 position = path.transform.TransformPoint(point.position);
Vector3 controlPoint = path.transform.TransformPoint(point.tangent);
//繪制切線
Handles.DrawDottedLine(position, controlPoint + position, 1f);
}
//恢復(fù)顏色
Handles.color = cacheColor;
}
//路徑點(diǎn)操作柄繪制
private void DrawPositionHandle(int index)
{
BezierCurvePoint point = path.Points[index];
//局部轉(zhuǎn)全局坐標(biāo)
Vector3 position = path.transform.TransformPoint(point.position);
//操作柄的旋轉(zhuǎn)類型
Quaternion rotation = Tools.pivotRotation == PivotRotation.Local
? path.transform.rotation : Quaternion.identity;
//操作柄的大小
float size = HandleUtility.GetHandleSize(position) * sphereHandleCapSize;
//在該路徑點(diǎn)繪制一個(gè)球形
Handles.color = Color.white;
Handles.SphereHandleCap(0, position, rotation, size, EventType.Repaint);
Handles.Label(position, string.Format("Point{0}", index));
//檢測變更
EditorGUI.BeginChangeCheck();
//坐標(biāo)操作柄
position = Handles.PositionHandle(position, rotation);
//變更檢測結(jié)束 如果發(fā)生變更 更新路徑點(diǎn)
if (EditorGUI.EndChangeCheck())
{
//記錄操作
Undo.RecordObject(path, "Position Changed");
//全局轉(zhuǎn)局部坐標(biāo)
point.position = path.transform.InverseTransformPoint(position);
//更新路徑點(diǎn)
path.Points[index] = point;
}
}
//控制點(diǎn)操作柄繪制
private void DrawTangentHandle(int index)
{
BezierCurvePoint point = path.Points[index];
//局部轉(zhuǎn)全局坐標(biāo)
Vector3 cp = path.transform.TransformPoint(point.position + point.tangent);
//操作柄的旋轉(zhuǎn)類型
Quaternion rotation = Tools.pivotRotation == PivotRotation.Local
? path.transform.rotation : Quaternion.identity;
//操作柄的大小
float size = HandleUtility.GetHandleSize(cp) * sphereHandleCapSize;
//在該控制點(diǎn)繪制一個(gè)球形
Handles.color = Color.yellow;
Handles.SphereHandleCap(0, cp, rotation, size, EventType.Repaint);
//檢測變更
EditorGUI.BeginChangeCheck();
//坐標(biāo)操作柄
cp = Handles.PositionHandle(cp, rotation);
//變更檢測結(jié)束 如果發(fā)生變更 更新路徑點(diǎn)
if (EditorGUI.EndChangeCheck())
{
//記錄操作
Undo.RecordObject(path, "Control Point Changed");
//全局轉(zhuǎn)局部坐標(biāo)
point.tangent = path.transform.InverseTransformPoint(cp) - point.position;
//更新路徑點(diǎn)
path.Points[index] = point;
}
}
}
#endif
}
SimpleBezierCurvePathAlonger
-
path
:貝塞爾曲線路徑; -
speed
:移動(dòng)速度; -
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
中:
文章來源:http://www.zghlxwxcb.cn/news/detail-818242.html
參考鏈接:文章來源地址http://www.zghlxwxcb.cn/news/detail-818242.html
- 貝塞爾曲線 - 百度百科
- Unity Cinemachine Path
- Unity 貝塞爾曲線(Beizer curve)的原理與運(yùn)用
到了這里,關(guān)于Bezier Curve 貝塞爾曲線 - 在Unity中實(shí)現(xiàn)路徑編輯的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!