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

时间:2023-03-31 16:52:12


简介

贝塞尔曲线(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;
#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

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

代码如下:

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

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

代码如下:

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
}

应用

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

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;
#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>
        /// 根据归一化位置值获取对应的贝塞尔曲线上的点
        /// </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)的原理与运用