貝塞爾曲線
目標:在unity中展示Bezier曲線妥衣。
貝塞爾曲線原理
用代碼實現(xiàn) bezier數(shù)學公式
-
Bezier公式(一般參數(shù)公式)
image.png
-
注解:
階貝茲曲線可如下推斷。給定點P0完箩、P1粘捎、…帖蔓、Pn檬贰,其貝茲曲線即:
如上公式可如下遞歸表達: 用表示由點P0、P1遵湖、…悔政、Pn所決定的貝茲曲線。
用平常話來說奄侠,階的貝茲曲線卓箫,即雙階貝茲曲線之間的插值。- 代碼實現(xiàn):
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; //Bezier點數(shù)據(jù)垄潮,包含該點的位置烹卒,以及該點的t參數(shù)闷盔。 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生命周期函數(shù) // 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; } //組合數(shù) public static int CombinationNum(int totality, int atATime) { return MathExtenders.Factorial(totality) / (MathExtenders.Factorial(atATime) * MathExtenders.Factorial(totality - atATime)); } } #endregion
- 代碼實現(xiàn):
-
切割Bezier
- 嘗試計算切割點
條件:切割三階Bezier旅急。
切割點好計算逢勾,同mesh切割。關(guān)鍵是在切割之后 計算出 Bezier點的外框點位置藐吮。
嘗試過 從切割出溺拱,取往后取兩個Bezier點的。通過方程組解出剩余的兩個外框點谣辞。
理論上應該可以迫摔,但是未成功∧啻樱【后面一次又成功了句占,原因:前一次精度不足】
-
公式推導:
bezier切割公式.png -
代碼:
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; // 用于顯示切割結(jié)果 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切割【且只能切割一次】 * 大致思路:有一個切割線段的 函數(shù)躯嫉、根據(jù)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($"計算結(jié)束:p1:{p1}翎猛,p2:{p2}"); } #region 參數(shù)計算函數(shù) 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
- 嘗試計算切割點
結(jié)果測試:
image.png
可以看到:橙色的部分的Bezier曲線 胖翰,就是當t=0.5時,切割后的線切厘。與原來的Bezier線完全重合萨咳。
我推導的公式的主要思路就是:計算出三階Bezier曲線的兩個錨點位置。通過方程組可以求解得出來疫稿。
該公式可以結(jié)合 切割算法培他。通過遍歷Bezier線的List鹃两,求出 被切割點的t 值。讓后帶入我推導出來的算法舀凛,即可得到切割后的兩個錨點位置俊扳。
點關(guān)注,不迷路