> 文章列表 > Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

文章目录

  • 简介
    • 一阶贝塞尔曲线
    • 二阶贝塞尔曲线
    • 三阶贝塞尔曲线
  • 图形理解 Bezier Curve
    • 一阶贝塞尔曲线
    • 二阶贝塞尔曲线
    • 三阶贝塞尔曲线
  • 应用
    • Bezier Curve
    • SimpleBezierCurvePath
    • SimpleBezierCurvePathAlonger

简介

贝塞尔曲线(Bezier Curve),又称贝兹曲线或贝济埃曲线,是计算机图形学中相当重要的参数曲线,在我们常用的软件如Photo Shop中就有贝塞尔曲线工具,本文简单介绍贝塞尔曲线在Unity中的实现与应用。

一阶贝塞尔曲线

给顶点P0、P1,只是一条两点之间的直线,公式如下:

B(t) = P0 + (P1 - P0) t = (1 - t) P0 + t P1, t ∈ [0, 1]

等同于线性插值,代码实现如下:

/// <summary>
/// 一阶贝塞尔曲线
/// </summary>
/// <param name="p0">起点</param>
/// <param name="p1">终点</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;
}

二阶贝塞尔曲线

路径由给定点P0、P1、P2的函数计算,公式如下:

B(t) = (1 - t)2 P0 + 2t (1 - t) P1 + t2P2, t ∈[0, 1]

代码实现如下:

/// <summary>
/// 二阶贝塞尔曲线
/// </summary>
/// <param name="p0">起点</param>
/// <param name="p1">控制点</param>
/// <param name="p2">终点</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四个点在平面或三维空间中定义了三次方贝塞尔曲线。曲线起始于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]

代码实现如下:

/// <summary>
/// 三阶贝塞尔曲线
/// </summary>
/// <param name="p0">起点</param>
/// <param name="p1">控制点1</param>
/// <param name="p2">控制点2</param>
/// <param name="p3">终点</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为起点,P1为终点,t从0到1时,在贝塞尔曲线上对应的点为Pt,可以将t为理解为动画播放中的normalized time

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

代码如下:

using UnityEngine;
using SK.Framework;#if UNITY_EDITOR
using UnityEditor;
#endifpublic class Example : MonoBehaviour
{private float t;private void Update(){if (t < 1f){t += Time.deltaTime * .2f;t = Mathf.Clamp01(t);}}#if UNITY_EDITORprivate 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

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

代码如下:

using UnityEngine;
using SK.Framework;#if UNITY_EDITOR
using UnityEditor;
#endifpublic class Example : MonoBehaviour
{private float t;private void Update(){if (t < 1f){t += Time.deltaTime * .2f;t = Mathf.Clamp01(t);}}#if UNITY_EDITORprivate 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

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

代码如下:

using UnityEngine;
using SK.Framework;#if UNITY_EDITOR
using UnityEditor;
#endifpublic class Example : MonoBehaviour
{private float t;private void Update(){if (t < 1f){t += Time.deltaTime * .2f;t = Mathf.Clamp01(t);}}#if UNITY_EDITORprivate 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
}

应用

常见的如道路编辑、河流编辑功能都可以通过贝塞尔曲线实现:

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

本文以一个简单的路径编辑为例,通过使用三阶贝塞尔曲线实现路径的编辑:

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

Bezier Curve

  • segments:贝塞尔曲线的段数,值越大曲线精度越高;
  • loop:是否循环(首尾相连);
  • points :点集合(结构体中包含坐标点和控制点);
using System;
using UnityEngine;
using System.Collections.Generic;namespace SK.Framework
{/// <summary>/// 贝塞尔曲线/// </summary>[Serializable]public class BezierCurve{/// <summary>/// 段数/// </summary>[Range(1, 100)] public int segments = 10;/// <summary>/// 是否循环/// </summary>public bool loop;/// <summary>/// 点集合/// </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>/// 根据归一化位置值获取对应的贝塞尔曲线上的点/// </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>/// 坐标点/// </summary>public Vector3 position;/// <summary>/// 控制点 与坐标点形成切线/// </summary>public Vector3 tangent;}
}

SimpleBezierCurvePath

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

using UnityEngine;
using System.Collections.Generic;#if UNITY_EDITOR
using UnityEditor;
#endifnamespace 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>/// 根据归一化位置值获取对应的贝塞尔曲线上的点/// </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;//缓存上个坐标点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
}

SimpleBezierCurvePathAlonger

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

  • 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;}}
}

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

源码已上传至SKFramework框架Package Manager中:

Bezier Curve 贝塞尔曲线 - 在Unity中实现路径编辑

参考链接:

  1. 贝塞尔曲线 - 百度百科
  2. Unity Cinemachine Path
  3. Unity 贝塞尔曲线(Beizer curve)的原理与运用