如果按照Bezier算法中的t值来获取位置来控制运动,通常这种运动不是匀速的,这个内容的核心是如何通过长度获取Bezier路径上的位置,目的是能够控制物体沿着Bezier路径匀速运动。当然目前的算法依旧是近似的,精度取决于interpolation插值的数量,总的来说插值高的话,运算量会比较大,但基本可控。
具体代码有三部分,第一部分是Bezier点计算类:
using UnityEngine;
public class Bezier
{
//三点贝塞尔
static public Vector3 GetPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2)
{
t = Mathf.Clamp01(t);
float s = 1f - t;
return s * s * p0 + 2f * s * t * p1 + t * t * p2;
}
//三点切线
static public Vector3 GetTangent(float t, Vector3 p0, Vector3 p1, Vector3 p2)
{
t = Mathf.Clamp01(t);
return 2f * (1f - t) * (p1 - p0) + 2f * t * (p2 - p1);
}
//四点贝塞尔
static public Vector3 GetPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
t = Mathf.Clamp01(t);
float s = 1f - t;
return s * s * s * p0 + 3f * s * s * t * p1 + 3f * s * t * t * p2 + t * t * t * p3;
}
//四点切线
static public Vector3 GetTangent(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
t = Mathf.Clamp01(t);
float s = 1f - t;
return 3f * s * s * (p1 - p0) + 6f * s * t * (p2 - p1) + 3f * t * t * (p3 - p2);
}
}
第二部分是BezierPath类:
using System.Collections.Generic;
using UnityEngine;
public class BezierPath
{
//BezierControlPoint就是Bezier控制点,这里假定了控制位置(controlPosition和oppositeCtrlPos)相对于点位置大小相等方向相反。
public class BezierControlPoint
{
Vector3 _position;
public Vector3 position { get { return _position; } }
Vector3 _relativeCtrlPos;
public Vector3 relativeCtrlPos { get { return _relativeCtrlPos; } }
public Vector3 controlPosition { get { return _position + _relativeCtrlPos; } }
public Vector3 oppositeCtrlPos { get { return _position - _relativeCtrlPos; } }
public BezierControlPoint() { _position = Vector3.zero; _relativeCtrlPos = Vector3.zero; }
public BezierControlPoint(Vector3 position, Vector3 relativeCtrlPos)
{
_position = position;
_relativeCtrlPos = relativeCtrlPos;
}
public void SetPosition(Vector3 position) { _position = position; }
public void SetRelativeCtrlPos(Vector3 relativeCtrlPos) { _relativeCtrlPos = relativeCtrlPos; }
}
//这是Bezier曲线烘焙后得到路径上的节点
public struct PathPoint
{
public Vector3 position;
public float length;
public PathPoint(Vector3 position, float length)
{
this.position = position;
this.length = length;
}
}
List<BezierControlPoint> listControlPoint;
public BezierControlPoint[] controlPoints { get { return listControlPoint.ToArray(); } }
//
int _interpolation = -1;
public int interpolation { get { return _interpolation; } }
public BezierPath(BezierControlPoint[] controlPoints, int interpolation)
{
if (controlPoints == null || controlPoints.Length == 0)
{
controlPoints = new BezierControlPoint[] { new BezierControlPoint(), new BezierControlPoint() };
}
else if (controlPoints.Length == 1)
{
BezierControlPoint bp0 = controlPoints[0];
BezierControlPoint bp1 = new BezierControlPoint(bp0.position, bp0.relativeCtrlPos);
controlPoints = new BezierControlPoint[] { bp0, bp1 };
}
//
listControlPoint = new List<BezierControlPoint>();
listControlPoint.AddRange(controlPoints);
//
SetInterpolation(interpolation);
}
List<PathPoint> listPathPoint;
public PathPoint[] pathPoints { get { return listPathPoint.ToArray(); } }
//调整BezierControlPoint的插值数量
public void SetInterpolation(int interpolation)
{
if (interpolation < 0) interpolation = 0;
if (interpolation == _interpolation) return;
_interpolation = interpolation;
//
UpdatePathPoints();
}
//重新烘培路径点
void UpdatePathPoints()
{
if (listPathPoint != null)
listPathPoint.Clear();
else
listPathPoint = new List<PathPoint>();
float pathLength = 0;
Vector3 pathPosition = listControlPoint[0].position;
Vector3 curPos;
float segLen;
PathPoint pp;
for (int i = 0; i < listControlPoint.Count - 1; i++)
{
Vector3 p1 = listControlPoint[i].position;
Vector3 p2 = listControlPoint[i].controlPosition;
Vector3 p3 = listControlPoint[i + 1].oppositeCtrlPos;
Vector3 p4 = listControlPoint[i + 1].position;
float fCount = _interpolation + 1f;
for (int j = 0; j <= _interpolation; j++)
{
float t = j / fCount;
curPos = Bezier.GetPoint(t, p1, p2, p3, p4);
segLen = Vector3.Distance(curPos, pathPosition);
pathLength += segLen;
pathPosition = curPos;
pp = new PathPoint(Bezier.GetPoint(t, p1, p2, p3, p4), pathLength);
listPathPoint.Add(pp);
}
}
//
curPos = listControlPoint[listControlPoint.Count - 1].position;
segLen = Vector3.Distance(curPos, pathPosition);
pathLength += segLen;
pp = new PathPoint(curPos, pathLength);
listPathPoint.Add(pp);
}
//调整BezierControlPoint的点位置
public void SetBezierPointPosition(int index, Vector3 position)
{
listControlPoint[index].SetPosition(position);
UpdatePathPoints();
}
//调整BezierControlPoint的控制位置
public void SetBezierPointRelativeCtrlPos(int index, Vector3 relativeCtrlPos)
{
listControlPoint[index].SetRelativeCtrlPos(relativeCtrlPos);
UpdatePathPoints();
}
//通过给点的长度,获取路径上的位置
public Vector3 GetPathPosition(float length)
{
PathPoint pathPointStart = listPathPoint[0];
if (length <= 0) return pathPointStart.position;
PathPoint pathPointLast = listPathPoint[listPathPoint.Count - 1];
if (length > pathPointLast.length) return pathPointLast.position;
PathPoint pathPointA = pathPointStart;
PathPoint pathPointB = pathPointLast;
int index = (int)(length / pathPointLast.length * listPathPoint.Count);
if (length < listPathPoint[index].length)
{
for (int i = index; i > 0; i--)
{
if (length < listPathPoint[i - 1].length) continue;
//
pathPointA = listPathPoint[i - 1];
pathPointB = listPathPoint[i];
break;
}
}
else
{
for (int i = index; i < listPathPoint.Count; i++)
{
if (length > listPathPoint[i].length) continue;
//
pathPointA = listPathPoint[i - 1];
pathPointB = listPathPoint[i];
break;
}
}
float segLen = pathPointB.length - pathPointA.length;
float subLen = length - pathPointA.length;
float progress = subLen / segLen;
return Vector3.Lerp(pathPointA.position, pathPointB.position, progress);
}
public float length { get { return listPathPoint[listPathPoint.Count - 1].length; } }
}
这个类里面获取路径上位置点的方法GetPathPosition我尽可能做了一些优化,但由于本人数学知识有限,肯定不会是最佳优化方法。
第三部分是对BezierPath类的使用案例:
using UnityEngine;
public class Test : MonoBehaviour
{
[SerializeField]
Transform tr00;
[SerializeField]
Transform tr01;
[SerializeField]
Transform tr02;
[SerializeField]
Transform tr03;
[SerializeField]
Transform tr04;
[SerializeField]
Transform tr05;
[SerializeField]
int interpolation = 10;
[SerializeField]
float radius = 1;
[SerializeField]
float length = 0;
private void OnDrawGizmos()
{
BezierPath.BezierControlPoint p0 = new BezierPath.BezierControlPoint(tr00.position, tr01.position - tr00.position);
BezierPath.BezierControlPoint p1 = new BezierPath.BezierControlPoint(tr02.position, tr03.position - tr02.position);
BezierPath.BezierControlPoint p2 = new BezierPath.BezierControlPoint(tr04.position, tr05.position - tr04.position);
BezierPath.BezierControlPoint[] bps = new BezierPath.BezierControlPoint[] { p0, p1, p2 };
BezierPath bezierPath = new BezierPath(bps, interpolation);
Gizmos.color = Color.green;
BezierPath.PathPoint[] pathPoints = bezierPath.pathPoints;
for (int i = 0; i < pathPoints.Length - 1; i++)
{
Gizmos.DrawLine(pathPoints[i].position, pathPoints[i + 1].position);
}
//
Gizmos.color = Color.cyan;
Gizmos.DrawLine(bps[0].position, bps[0].controlPosition);
for (int i = 1; i < bps.Length - 1; i++)
{
Gizmos.DrawLine(bps[i].position, bps[i].controlPosition);
Gizmos.DrawLine(bps[i].position, bps[i].oppositeCtrlPos);
}
Gizmos.DrawLine(bps[bps.Length - 1].position, bps[bps.Length - 1].oppositeCtrlPos);
//
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(bps[0].position, radius);
Gizmos.DrawWireSphere(bps[0].controlPosition, radius);
for (int i = 1; i < bps.Length - 1; i++)
{
Gizmos.DrawWireSphere(bps[i].position, radius);
Gizmos.DrawWireSphere(bps[i].controlPosition, radius);
Gizmos.DrawWireSphere(bps[i].oppositeCtrlPos, radius);
}
Gizmos.DrawWireSphere(bps[bps.Length - 1].position, radius);
Gizmos.DrawWireSphere(bps[bps.Length - 1].oppositeCtrlPos, radius);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(bezierPath.GetPathPosition(length), radius);
}
}
这脚本挂到物体上,调整length值,可以看看效果。