/// <summary> /// Shifts the spline with <paramref name="amount"/> distance and returns a new spline /// </summary> /// <param name="safeDist"> /// The max distance between the middle of the curves and their extreme points. /// If your shifted curve has too many sharp edges try pumping this number up. /// But beware because it causes it to have more points and thus decreasing performance. /// </param> /// <param name="newSpline">If you want the returned spline to be put into an already existing spline assign this parameter.</param> public BezierSpline GetShiftedSpline(float amount, float safeDist = 0.3f, BezierSpline newSpline = null) { BezierSpline _shiftedSpline; if (newSpline != null) { _shiftedSpline = newSpline; } else { _shiftedSpline = new GameObject().AddComponent <BezierSpline>(); } _shiftedSpline.Clear(); _shiftedSpline.name = "Lane" + amount; // So, you cannot offset a Bézier curve perfectly with another Bézier curve, no matter how high-order you make that // other Bézier curve. However, we can chop up a curve into "safe" sub-curves (where safe means that all the control // points are always on a single side of the baseline, and the midpoint of the curve at t=0.5 is roughly in the centre // of the polygon defined by the curve coordinates) and then point-scale each sub-curve with respect to its scaling // origin (which is the intersection of the point normals at the start and end points). // A good way to do this reduction is to first find the curve's extreme points, and use these as initial splitting points. // After this initial split, we can check each individual segment to see if it's "safe enough" based on where the // center of the curve is.If the on-curve point for t = 0.5 is too far off from the center, we simply split the // segment down the middle. Generally this is more than enough to end up with safe segments. Vector3 _p0 = points[0]; Vector3 _p1, _p2, _p3; List <Vector3[]> _newPointsSpline = new List <Vector3[]>(); for (int i = 3; i < PointCount; i += 3) { _p1 = points[i - 2]; _p2 = points[i - 1]; _p3 = points[i]; // roots of our cubic bezier curve to find extremes SortedSet <float> _solutions = new SortedSet <float>(); { float?_sol1, _sol2; BezierMath.CubicGetRoots(_p0.x, _p1.x, _p2.x, _p3.x, out _sol1, out _sol2); if (_sol1.HasValue && _sol1.Value < 1 && _sol1.Value > 0) { _solutions.Add(_sol1.Value); } if (_sol2.HasValue && _sol2.Value < 1 && _sol2.Value > 0) { _solutions.Add(_sol2.Value); } BezierMath.CubicGetRoots(_p0.y, _p1.y, _p2.y, _p3.y, out _sol1, out _sol2); if (_sol1.HasValue && _sol1.Value < 1 && _sol1.Value > 0) { _solutions.Add(_sol1.Value); } if (_sol2.HasValue && _sol2.Value < 1 && _sol2.Value > 0) { _solutions.Add(_sol2.Value); } BezierMath.CubicGetRoots(_p0.z, _p1.z, _p2.z, _p3.z, out _sol1, out _sol2); if (_sol1.HasValue && _sol1.Value < 1 && _sol1.Value > 0) { _solutions.Add(_sol1.Value); } if (_sol2.HasValue && _sol2.Value < 1 && _sol2.Value > 0) { _solutions.Add(_sol2.Value); } } // the list of new points of the offset curve List <Vector3[]> _newPointsCurve = new List <Vector3[]>(); // So we're gonna need to split the starting curve at potentially more points // We do that by splitting in order and always taking the second split curve as the next // starting curve { Vector3[] _first, _second = new Vector3[] { _p0, _p1, _p2, _p3 }; float _secondSize = 1f; // we need this to know how big the second curve is compared to the very first starting curve // so we can split at the right point foreach (float sol in _solutions) { BezierMath.CubicSplitCurve(_second[0], _second[1], _second[2], _second[3], _secondSize * sol, out _first, out _second); _secondSize *= (1f - sol); _newPointsCurve.Add(_first); } _newPointsCurve.Add(_second); // no more calculations with the second curve, so we can add it to the list } // check whether each bezier curve is safe or not if not split it { int _at = _newPointsCurve.Count - 1; while (_at >= 0) { Vector3[] _split1, _split2; // where the curv t = 0.5f Vector3 _zeroDotFive = BezierMath.CubicGetPoint(_newPointsCurve[_at][0], _newPointsCurve[_at][1], _newPointsCurve[_at][2], _newPointsCurve[_at][3], 0.5f); // the center of the curve's 4 points Vector3 _center = (_newPointsCurve[_at][0] + _newPointsCurve[_at][1] + _newPointsCurve[_at][2] + _newPointsCurve[_at][3]) / 4f; // if they are too far away if (Vector3.Distance(_zeroDotFive, _center) > safeDist) { // split curva at 0.5f BezierMath.CubicSplitCurve(_newPointsCurve[_at][0], _newPointsCurve[_at][1], _newPointsCurve[_at][2], _newPointsCurve[_at][3], 0.5f, out _split1, out _split2); // overwrite current curve _newPointsCurve[_at] = _split2; // add one before it _newPointsCurve.Insert(_at, _split1); // we need to check the _split too again as well, so add one to at _at++; } else { // no problem with this curve, move on _at--; } } } // scale { Vector3 _pivot; for (int k = 0; k < _newPointsCurve.Count; k++) { // get pivot -> the intersection of the point normals at the start and end points bool _doIntersect = Math3D.LineLineIntersection( out _pivot, _newPointsCurve[k][0], (Quaternion.LookRotation(Vector3.right) * BezierMath.CubicGetFirstDerivative(_newPointsCurve[k][0], _newPointsCurve[k][1], _newPointsCurve[k][2], _newPointsCurve[k][3], 0.01f)).normalized, _newPointsCurve[k][3], (Quaternion.LookRotation(Vector3.right) * BezierMath.CubicGetFirstDerivative(_newPointsCurve[k][0], _newPointsCurve[k][1], _newPointsCurve[k][2], _newPointsCurve[k][3], 0.99f)).normalized ); // scaling if (!_doIntersect) // it's essentially a line // which way the right is from the line { Vector3 _whichWay = Vector3.Cross(_newPointsCurve[k][3] - _newPointsCurve[k][0], Vector3.up).normalized; for (int j = 0; j < _newPointsCurve[k].Length; j++) { _newPointsCurve[k][j] -= _whichWay * amount; } } else { // cache this Vector3 _middleDerivative = BezierMath.CubicGetFirstDerivative( _newPointsCurve[k][0], _newPointsCurve[k][1], _newPointsCurve[k][2], _newPointsCurve[k][3], 0.5f); // which way is the middle point's right Vector3 _curveMiddleRight = (Quaternion.LookRotation(Vector3.right) * _middleDerivative).normalized; // which way is the middle point's left Vector3 _curveMiddleLeft = (Quaternion.LookRotation(Vector3.left) * _middleDerivative).normalized; for (int j = 0; j < _newPointsCurve[k].Length; j++) { Vector3 _scaleDir = (_pivot - _newPointsCurve[k][j]).normalized; // pivot is to the right bool _isPivotToRight = Vector3.Angle(_scaleDir, _curveMiddleRight) < Vector3.Angle(_scaleDir, _curveMiddleLeft); // move to position based on pivot direction if (_isPivotToRight) { _newPointsCurve[k][j] += _scaleDir * amount; } else { _newPointsCurve[k][j] -= _scaleDir * amount; } } } } } _newPointsSpline.AddRange(_newPointsCurve); _p0 = _p3; } _shiftedSpline.points = new Vector3[_newPointsSpline.Count * 3 + 1]; _shiftedSpline.points[0] = _newPointsSpline[0][0]; _shiftedSpline.modes = new BezierControlPointMode[_newPointsSpline.Count + 1]; _shiftedSpline.modes[0] = BezierControlPointMode.Free; for (int i = 0; i < _newPointsSpline.Count; i++) { _shiftedSpline.modes[i + 1] = BezierControlPointMode.Free; for (int k = 1; k <= 3; k++) { _shiftedSpline.points[i * 3 + k] = _newPointsSpline[i][k]; } } return(_shiftedSpline); }