贝塞尔曲线
目标:在unity中展示Bezier曲线。
贝塞尔曲线原理
用代码实现 bezier数学公式
-
Bezier公式(一般参数公式)
-
注解:
阶贝兹曲线可如下推断。给定点P0、P1、…、Pn,其贝兹曲线即:
如上公式可如下递归表达: 用表示由点P0、P1、…、Pn所决定的贝兹曲线。
用平常话来说,阶的贝兹曲线,即双阶贝兹曲线之间的插值。- 代码实现:
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; //Bezier点数据,包含该点的位置,以及该点的t参数。 public struct BezierPos { public Vector3 bp; public float t; public BezierPos(Vector3 _pos) { this.bp = _pos; this.t = 0f; } public static BezierPos Zero { get { return new BezierPos(Vector3.zero); } } } public class Bezier : MonoBehaviour { private bool isStart; private List<Transform> _Target; private List<Vector2> vector2s; private List<BezierPos> bezierPoints; private OperateBezier operateBezier; #region unity生命周期函数 // Start is called before the first frame update void Start() { isStart = true; _Target = new List<Transform>(); vector2s = new List<Vector2>(); bezierPoints = new List<BezierPos>(); operateBezier = new OperateBezier(bezierPoints, vector2s); Vector2 buff = Vector2.zero; foreach (Transform item in transform) { buff.x = item.position.x; buff.y = item.position.z; vector2s.Add(buff); _Target.Add(item); } for (int i = 0; i < 20; i++) { bezierPoints.Add(BezierPos.Zero); } //print(bezierPoints.Count); //Frame(); //foreach (var item in bezierPoints) //{ // print(item); //} } // Update is called once per frame void Update() { Frame(); } public void Frame() { Vector2 buff = Vector2.zero; for (int i = 0; i < _Target.Count; i++) { buff.x = _Target[i].position.x; buff.y = _Target[i].position.z; vector2s[i] = buff; } operateBezier.CalculateOnce(); } private void OnDrawGizmos() { if (!isStart) return; Gizmos.color = Color.blue; for (int i = 0; i < bezierPoints.Count; i++) { //Gizmos.DrawLine(bezierPoints[i], bezierPoints[i + 1]); Gizmos.DrawCube(bezierPoints[i].bp, Vector3.one / 5); } } #endregion } #region bezier曲线的计算核心 public class OperateBezier { public List<BezierPos> pointList; // 曲线点 public List<Vector2> bezierHandList; // 外框点 public OperateBezier(List<BezierPos> beziers, List<Vector2> hands) { this.pointList = beziers; this.bezierHandList = hands; } public Vector2 OperateP(float t) { Vector2 _result = Vector2.zero; int n = bezierHandList.Count - 1; if (t > 1 || t < 0) { return _result; } for (int i = 0; i < bezierHandList.Count; i++) { _result += bezierHandList[i] * Mathf.Pow(1 - t, n - i) * Mathf.Pow(t, i) * MathExtenders.CombinationNum(n, i); //Debug.Log("------" + bezierHandList[i] + "输出" + _result); } return _result; } // 计算一次曲线。 public void CalculateOnce() { int pNum = pointList.Count - 1; float t; Vector2 buff1 = Vector2.zero; BezierPos buff2 = BezierPos.Zero; for (int i = 0; i < pointList.Count; i++) { t = (float)i / (float)pNum; buff1 = OperateP(t); //Debug.Log(buff1); buff2.bp.x = buff1.x; buff2.bp.z = buff1.y; buff2.t = t; pointList[i] = buff2; } } } #endregion #region 对unity做扩展 public static class Extends { public static Vector2 v3tov2 (this Vector3 v3) { return new Vector2(v3.x, v3.z); } public static Vector3 v2tov3(this Vector2 v2) { return new Vector3(v2.x, 0, v2.y); } } public class MathExtenders { //阶乘 public static int Factorial(int num) { if (num < 0) { return -1; } else if (num == 0) { return 1; } int _value = 1; for (int i = num; i > 0; i--) { _value *= i; } return _value; } //组合数 public static int CombinationNum(int totality, int atATime) { return MathExtenders.Factorial(totality) / (MathExtenders.Factorial(atATime) * MathExtenders.Factorial(totality - atATime)); } } #endregion
- 代码实现:
-
切割Bezier
- 尝试计算切割点
条件:切割三阶Bezier。
切割点好计算,同mesh切割。关键是在切割之后 计算出 Bezier点的外框点位置。
尝试过 从切割出,取往后取两个Bezier点的。通过方程组解出剩余的两个外框点。
理论上应该可以,但是未成功。【后面一次又成功了,原因:前一次精度不足】
-
公式推导:
-
代码:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cut : MonoBehaviour { // Start is called before the first frame update public Bezier r_bezierOrigin; // 起源bezier public Bezier r_bezierShow; // 用于显示切割结果 bezier private CutMath _cutMath; public float t0; public float t1; public float t2; public Vector2 p0; public Vector2 p3; public Vector2 B1; public Vector2 B2; void Start() { _cutMath = new CutMath(); } // Update is called once per frame void Update() { if (Input.GetKeyDown(KeyCode.A)) { OperateCut(); }else if (Input.GetKeyDown(KeyCode.B)) { print("t0:" + T2t(t0)); print("t1:" + T2t(t1)); print("t2:" + T2t(t2)); } } public float T2t(float _t) { float b = t0 / (1 - t0); _t = (_t / (1 - t0)) - b; return _t; } public void OperateCut() { this.t0 = r_bezierOrigin.BezierPoints[9].t; this.t1 = r_bezierOrigin.BezierPoints[10].t; this.t2 = r_bezierOrigin.BezierPoints[11].t; this.t1 = T2t(this.t1); this.t2 = T2t(this.t2); this.p0 = r_bezierOrigin.BezierPoints[9].bp.v3tov2(); this.p3 = r_bezierOrigin.BezierPoints[19].bp.v3tov2(); this.B1 = r_bezierOrigin.BezierPoints[10].bp.v3tov2(); this.B2 = r_bezierOrigin.BezierPoints[11].bp.v3tov2(); _cutMath.Start(t1, t2, p0, p3, B1, B2); print($"p1:{_cutMath.p1}、p2:{_cutMath.p2}"); r_bezierShow.Target[0].position = this.p0.v2tov3(); r_bezierShow.Target[1].position = _cutMath.p1.v2tov3(); r_bezierShow.Target[2].position = _cutMath.p2.v2tov3(); r_bezierShow.Target[3].position = this.p3.v2tov3(); } } #region 切割核心算法 /* 三阶bezier切割【且只能切割一次】 * 大致思路:有一个切割线段的 函数、根据bezier曲线的收尾外框点,判断存在切割,则开始切割 * 开始切割,遍历Bezier的线点,两个一个线段,判断是否切割。 * 如该出存在切割,则开始切割。三阶变wei * * 切割只支持 三阶,P0、1、2、3 四个点。 需要用算法 解出切割后的 P1、2点。 * */ // 计算 P1、2 public class CutMath { public float t1; public float t2; public Vector2 p0; public Vector2 p1; //待求 public Vector2 p2; // 待求 public Vector2 p3; public Vector2 B1; public Vector2 B2; private float k1, k2, l1, l2, j1, j2, i1, i2; private Vector2 E; private float T; public void Start(float _t1, float _t2, Vector2 _p0, Vector2 _p3, Vector2 _B1, Vector2 _B2) { //初始化 this.t1 = _t1; this.t2 = _t2; this.p0 = _p0; this.p3 = _p3; this.B1 = _B1; this.B2 = _B2; //计算 this.k1 = ComputeK(t1); this.k2 = ComputeK(t2); this.l1 = ComputeL(t1); this.l2 = ComputeL(t2); this.j1 = ComputeJ(t1); this.j2 = ComputeJ(t2); this.i1 = ComputeI(t1); this.i2 = ComputeI(t2); this.E = ComputeE(); this.T = ComputeT(); this.p2 = ComputeP2(); this.p1 = ComputeP1(); Debug.Log($"计算结束:p1:{p1},p2:{p2}"); } #region 参数计算函数 private float ComputeK(float t) { return Mathf.Pow(1 - t, 3); } private float ComputeL(float t) { return Mathf.Pow(1 - t, 2) * t * 3; } private float ComputeJ(float t) { return Mathf.Pow(t, 2) * (1 - t) * 3; } private float ComputeI(float t) { return Mathf.Pow(t, 3); } private Vector2 ComputeE() { return (B2 - k2 * p0 - i2 * p3) / l2; } private float ComputeT() { return j2 / l2; } private Vector2 ComputeP2() { return (B1 - k1 * p0 - l1 * E - i1 * p3) / (j1 - l1 * T); } private Vector2 ComputeP1() { return E - T * p2; } #endregion } #endregion
- 尝试计算切割点
结果测试:
可以看到:橙色的部分的Bezier曲线 ,就是当t=0.5时,切割后的线。与原来的Bezier线完全重合。
我推导的公式的主要思路就是:计算出三阶Bezier曲线的两个锚点位置。通过方程组可以求解得出来。
该公式可以结合 切割算法。通过遍历Bezier线的List,求出 被切割点的t 值。让后带入我推导出来的算法,即可得到切割后的两个锚点位置。
点关注,不迷路