using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace BansheeGz.BGSpline.Curve
|
|
{
|
|
/// <summary>
|
|
/// Basic math operations for curves (Distance, Position, Tangent, Closest point ).
|
|
/// It caches some data for quick access and recalculate it if curve changes.
|
|
/// It uses uniform split with forward differencing algorithm, which is the fastest available (for uniform split).
|
|
/// </summary>
|
|
public class BGCurveBaseMath : BGCurveMathI, IDisposable
|
|
{
|
|
#region events
|
|
|
|
//===============================================================================================
|
|
// Events
|
|
//===============================================================================================
|
|
|
|
/// <summary> Change was requested by changed curve's data </summary>
|
|
public event EventHandler ChangeRequested;
|
|
|
|
/// <summary> Math was changed</summary>
|
|
public event EventHandler Changed;
|
|
|
|
#endregion
|
|
|
|
#region fields
|
|
|
|
//===============================================================================================
|
|
// Fields
|
|
//===============================================================================================
|
|
|
|
/// <summary> all possible attributes (fields) to calculate</summary>
|
|
public enum Field
|
|
{
|
|
Position = 1,
|
|
Tangent = 2,
|
|
}
|
|
|
|
/// <summary> which fields to precalculate </summary>
|
|
public enum Fields
|
|
{
|
|
Position = 1,
|
|
PositionAndTangent = 3,
|
|
}
|
|
|
|
//curve
|
|
protected readonly BGCurve curve;
|
|
//what and how to calc
|
|
protected Config config;
|
|
|
|
//cached data for length calculations
|
|
protected readonly List<SectionInfo> cachedSectionInfos = new List<SectionInfo>();
|
|
|
|
// pools for sections
|
|
protected readonly List<SectionInfo> poolSectionInfos = new List<SectionInfo>();
|
|
// pools for points
|
|
protected readonly List<SectionPointInfo> poolPointInfos = new List<SectionPointInfo>();
|
|
|
|
//curve's length
|
|
protected float cachedLength;
|
|
|
|
//should we calculate position? (This is probably always true)
|
|
protected bool cachePosition;
|
|
//should we calculate tangent?
|
|
protected bool cacheTangent;
|
|
|
|
//closest point calculator
|
|
protected BGCurveCalculatorClosestPoint closestPointCalculator;
|
|
|
|
// frame when data was recalculated
|
|
private int recalculatedAtFrame = -1;
|
|
// frame when math was created
|
|
private int createdAtFrame;
|
|
|
|
// force section recalculation, even if it's not changed
|
|
protected bool ignoreSectionChangedCheck;
|
|
|
|
//some coefficients (these are used by approximation algorithm)
|
|
private double h;
|
|
private double h2;
|
|
private double h3;
|
|
|
|
/// <summary>Suppress all warnings, printed to the log </summary>
|
|
public bool SuppressWarning { get; set; }
|
|
|
|
/// <summary>get Curve</summary>
|
|
public BGCurve Curve
|
|
{
|
|
get { return curve; }
|
|
}
|
|
|
|
/// <summary>Get calculated sections data</summary>
|
|
public List<SectionInfo> SectionInfos
|
|
{
|
|
get { return cachedSectionInfos; }
|
|
}
|
|
|
|
/// <summary>Access calculated section's data by index</summary>
|
|
public SectionInfo this[int i]
|
|
{
|
|
get { return cachedSectionInfos[i]; }
|
|
}
|
|
|
|
/// <summary>How much sections in total</summary>
|
|
public int SectionsCount
|
|
{
|
|
get { return cachedSectionInfos.Count; }
|
|
}
|
|
|
|
/// <summary>Get last used config</summary>
|
|
public Config Configuration
|
|
{
|
|
get { return config; }
|
|
}
|
|
|
|
// if tangent is calculated and formula is needed
|
|
protected bool NeedTangentFormula
|
|
{
|
|
get { return !config.UsePointPositionsToCalcTangents && cacheTangent; }
|
|
}
|
|
|
|
/// <summary> How much approximation points are used in total </summary>
|
|
public int PointsCount
|
|
{
|
|
get
|
|
{
|
|
if (SectionsCount == 0) return 0;
|
|
var count = 0;
|
|
var length = cachedSectionInfos.Count;
|
|
for (var i = 0; i < length; i++) count += cachedSectionInfos[i].PointsCount;
|
|
return count;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region constructors
|
|
|
|
//===============================================================================================
|
|
// Constructors
|
|
//===============================================================================================
|
|
|
|
/// <summary> Math with default config settings </summary>
|
|
public BGCurveBaseMath(BGCurve curve) : this(curve, new Config(Fields.Position))
|
|
{
|
|
}
|
|
|
|
/// <summary> Math with config settings </summary>
|
|
public BGCurveBaseMath(BGCurve curve, Config config)
|
|
{
|
|
this.curve = curve;
|
|
curve.Changed += CurveChanged;
|
|
Init(config ?? new Config(Fields.Position));
|
|
}
|
|
|
|
/// <summary>This is an old constructor, left for compatibility. Only Fields.PositionWorld is calculated.
|
|
/// Use another constructor to specify which fields and how need to be calculated. </summary>
|
|
[Obsolete("Use another constructors")]
|
|
public BGCurveBaseMath(BGCurve curve, bool traceChanges, int parts = 30, bool usePointPositionsToCalcTangents = false)
|
|
: this(curve, new Config(Fields.Position) {Parts = parts, UsePointPositionsToCalcTangents = usePointPositionsToCalcTangents})
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Init
|
|
|
|
//===============================================================================================
|
|
// Init
|
|
//===============================================================================================
|
|
/// <summary>Init math with new config and recalculate it's data </summary>
|
|
public virtual void Init(Config config)
|
|
{
|
|
//if we calculated positions before (but not tangent) we should skip the check if section was changed or not and force recalculation (cause positions may not change)
|
|
if (this.config != null)
|
|
{
|
|
ignoreSectionChangedCheck = this.config.Fields == Fields.Position && config.Fields == Fields.PositionAndTangent;
|
|
this.config.Update -= ConfigOnUpdate;
|
|
}
|
|
else
|
|
{
|
|
ignoreSectionChangedCheck = false;
|
|
}
|
|
|
|
//assign new config
|
|
this.config = config;
|
|
config.Parts = Mathf.Clamp(config.Parts, 1, 1000);
|
|
this.config.Update += ConfigOnUpdate;
|
|
|
|
createdAtFrame = Time.frameCount;
|
|
cachePosition = Field.Position.In(config.Fields.Val());
|
|
cacheTangent = Field.Tangent.In(config.Fields.Val());
|
|
|
|
//No fields- no need to calculate or trace anything.
|
|
if (!cachePosition && !cacheTangent)
|
|
throw new UnityException("No fields were chosen. Create math like this: new BGCurveBaseMath(curve, new BGCurveBaseMath.Config(BGCurveBaseMath.Fields.Position))");
|
|
|
|
//some additional init steps for subclasses
|
|
AfterInit(config);
|
|
|
|
//calculate data, based on new config
|
|
Recalculate(true);
|
|
}
|
|
|
|
//some additional initialization for subclasses
|
|
protected virtual void AfterInit(Config config)
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public methods
|
|
|
|
//===============================================================================================
|
|
// Public methods
|
|
//===============================================================================================
|
|
|
|
//=========================================== Calculate by formula
|
|
|
|
/// <summary> Calculate point world position between 2 points by a formula. This is a slow method</summary>
|
|
/// <param name="t">Ratio between (0,1)</param>
|
|
/// <param name="useLocal">Use local coordinates instead of world</param>
|
|
public virtual Vector3 CalcPositionByT(BGCurvePoint @from, BGCurvePoint to, float t, bool useLocal = false)
|
|
{
|
|
t = Mathf.Clamp01(t);
|
|
|
|
var fromPos = useLocal ? from.PositionLocal : from.PositionWorld;
|
|
var toPos = useLocal ? to.PositionLocal : to.PositionWorld;
|
|
|
|
Vector3 result;
|
|
if (from.ControlType == BGCurvePoint.ControlTypeEnum.Absent && to.ControlType == BGCurvePoint.ControlTypeEnum.Absent)
|
|
{
|
|
//lerp
|
|
result = fromPos + ((toPos - fromPos)*t);
|
|
}
|
|
else
|
|
{
|
|
var fromPosHandle = useLocal ? from.ControlSecondLocal + fromPos : from.ControlSecondWorld;
|
|
var toPosHandle = useLocal ? to.ControlFirstLocal + toPos : to.ControlFirstWorld;
|
|
|
|
result = (from.ControlType != BGCurvePoint.ControlTypeEnum.Absent && to.ControlType != BGCurvePoint.ControlTypeEnum.Absent)
|
|
? BGCurveFormulas.BezierCubic(t, fromPos, fromPosHandle, toPosHandle, toPos)
|
|
: BGCurveFormulas.BezierQuadratic(t, fromPos, (@from.ControlType == BGCurvePoint.ControlTypeEnum.Absent) ? toPosHandle : fromPosHandle, toPos);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary> Calculate a tangent between 2 points by a formula. This is a slow method</summary>
|
|
/// <param name="t">Ratio between (0,1)</param>
|
|
/// <param name="useLocal">Use local coordinates instead of world</param>
|
|
public virtual Vector3 CalcTangentByT(BGCurvePoint @from, BGCurvePoint to, float t, bool useLocal = false)
|
|
{
|
|
if (Curve.PointsCount < 2) return Vector3.zero;
|
|
|
|
t = Mathf.Clamp01(t);
|
|
|
|
var fromPos = useLocal ? from.PositionLocal : from.PositionWorld;
|
|
var toPos = useLocal ? to.PositionLocal : to.PositionWorld;
|
|
|
|
Vector3 result;
|
|
if (from.ControlType == BGCurvePoint.ControlTypeEnum.Absent && to.ControlType == BGCurvePoint.ControlTypeEnum.Absent)
|
|
{
|
|
result = toPos - fromPos;
|
|
}
|
|
else
|
|
{
|
|
var fromPosHandle = useLocal ? from.ControlSecondLocal + fromPos : from.ControlSecondWorld;
|
|
var toPosHandle = useLocal ? to.ControlFirstLocal + toPos : to.ControlFirstWorld;
|
|
|
|
result = (from.ControlType != BGCurvePoint.ControlTypeEnum.Absent && to.ControlType != BGCurvePoint.ControlTypeEnum.Absent)
|
|
? BGCurveFormulas.BezierCubicDerivative(t, fromPos, fromPosHandle, toPosHandle, toPos)
|
|
: BGCurveFormulas.BezierQuadraticDerivative(t, fromPos, (@from.ControlType == BGCurvePoint.ControlTypeEnum.Absent) ? toPosHandle : fromPosHandle, toPos);
|
|
}
|
|
|
|
return result.normalized;
|
|
}
|
|
|
|
|
|
//=========================================== Generic methods
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcByDistanceRatio(float distanceRatio, out Vector3 tangent, bool useLocal = false)
|
|
{
|
|
return CalcByDistance(cachedLength*distanceRatio, out tangent, useLocal);
|
|
}
|
|
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcByDistance(float distance, out Vector3 tangent, bool useLocal = false)
|
|
{
|
|
if (distance < 0f) distance = 0f;
|
|
else if (distance > cachedLength) distance = cachedLength;
|
|
|
|
Vector3 position;
|
|
BinarySearchByDistance(distance, out position, out tangent, true, true);
|
|
|
|
if (useLocal)
|
|
{
|
|
position = curve.transform.InverseTransformPoint(position);
|
|
tangent = curve.transform.InverseTransformDirection(tangent);
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcByDistanceRatio(Field field, float distanceRatio, bool useLocal = false)
|
|
{
|
|
return CalcByDistance(field, cachedLength*distanceRatio, useLocal);
|
|
}
|
|
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcByDistance(Field field, float distance, bool useLocal = false)
|
|
{
|
|
var pointsCount = curve.PointsCount;
|
|
switch (pointsCount)
|
|
{
|
|
case 0:
|
|
return Vector3.zero;
|
|
case 1:
|
|
return field == Field.Position ? curve[0].PositionWorld : Vector3.zero;
|
|
}
|
|
|
|
if (distance < 0f) distance = 0f;
|
|
else if (distance > cachedLength) distance = cachedLength;
|
|
|
|
var calcPosition = field == Field.Position;
|
|
Vector3 position, tangent;
|
|
BinarySearchByDistance(distance, out position, out tangent, calcPosition, !calcPosition);
|
|
|
|
if (useLocal)
|
|
{
|
|
switch (field)
|
|
{
|
|
case Field.Position:
|
|
position = curve.transform.InverseTransformPoint(position);
|
|
break;
|
|
case Field.Tangent:
|
|
tangent = curve.transform.InverseTransformDirection(tangent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return calcPosition ? position : tangent;
|
|
}
|
|
|
|
//=========================================== Position and Tangent
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcPositionAndTangentByDistanceRatio(float distanceRatio, out Vector3 tangent, bool useLocal = false)
|
|
{
|
|
return CalcByDistanceRatio(distanceRatio, out tangent, useLocal);
|
|
}
|
|
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcPositionAndTangentByDistance(float distance, out Vector3 tangent, bool useLocal = false)
|
|
{
|
|
return CalcByDistance(distance, out tangent, useLocal);
|
|
}
|
|
|
|
//=========================================== Position
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcPositionByDistanceRatio(float distanceRatio, bool useLocal = false)
|
|
{
|
|
return CalcByDistanceRatio(Field.Position, distanceRatio, useLocal);
|
|
}
|
|
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcPositionByDistance(float distance, bool useLocal = false)
|
|
{
|
|
return CalcByDistance(Field.Position, distance, useLocal);
|
|
}
|
|
|
|
//=========================================== Tangent
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcTangentByDistanceRatio(float distanceRatio, bool useLocal = false)
|
|
{
|
|
return CalcByDistanceRatio(Field.Tangent, distanceRatio, useLocal);
|
|
}
|
|
|
|
//using useLocal is significantly slower. See interface for more comments
|
|
public virtual Vector3 CalcTangentByDistance(float distance, bool useLocal = false)
|
|
{
|
|
return CalcByDistance(Field.Tangent, distance, useLocal);
|
|
}
|
|
|
|
|
|
//=========================================== Find Secton's index
|
|
|
|
//see interface for comments
|
|
public int CalcSectionIndexByDistance(float distance)
|
|
{
|
|
return FindSectionIndexByDistance(ClampDistance(distance));
|
|
}
|
|
|
|
//see interface for comments
|
|
public int CalcSectionIndexByDistanceRatio(float ratio)
|
|
{
|
|
return FindSectionIndexByDistance(DistanceByRatio(ratio));
|
|
}
|
|
|
|
//=========================================== Closest Point
|
|
|
|
//see interface for comments
|
|
public Vector3 CalcPositionByClosestPoint(Vector3 point, bool skipSectionsOptimization = false, bool skipPointsOptimization = false)
|
|
{
|
|
if (closestPointCalculator == null) closestPointCalculator = new BGCurveCalculatorClosestPoint(this);
|
|
float distance;
|
|
Vector3 tangent;
|
|
return closestPointCalculator.CalcPositionByClosestPoint(point, out distance, out tangent, skipSectionsOptimization, skipPointsOptimization);
|
|
}
|
|
|
|
//see interface for comments
|
|
public Vector3 CalcPositionByClosestPoint(Vector3 point, out float distance, bool skipSectionsOptimization = false, bool skipPointsOptimization = false)
|
|
{
|
|
if (closestPointCalculator == null) closestPointCalculator = new BGCurveCalculatorClosestPoint(this);
|
|
Vector3 tangent;
|
|
return closestPointCalculator.CalcPositionByClosestPoint(point, out distance, out tangent, skipSectionsOptimization, skipPointsOptimization);
|
|
}
|
|
|
|
//see interface for comments
|
|
public Vector3 CalcPositionByClosestPoint(Vector3 point, out float distance, out Vector3 tangent, bool skipSectionsOptimization = false, bool skipPointsOptimization = false)
|
|
{
|
|
if (closestPointCalculator == null) closestPointCalculator = new BGCurveCalculatorClosestPoint(this);
|
|
return closestPointCalculator.CalcPositionByClosestPoint(point, out distance, out tangent, skipSectionsOptimization, skipPointsOptimization);
|
|
}
|
|
|
|
|
|
//=========================================== Total Distance
|
|
//see interface for comments
|
|
public virtual float GetDistance()
|
|
{
|
|
return cachedLength;
|
|
}
|
|
|
|
//=========================================== Curve's point world coordinates (faster then using point.positionWord etc.)
|
|
|
|
/// <summary> Get point's world position </summary>
|
|
public Vector3 GetPosition(int pointIndex)
|
|
{
|
|
var count = cachedSectionInfos.Count;
|
|
|
|
if (count == 0 || count <= pointIndex) return curve[pointIndex].PositionWorld;
|
|
|
|
return pointIndex < count ? cachedSectionInfos[pointIndex].OriginalFrom : cachedSectionInfos[pointIndex - 1].OriginalTo;
|
|
}
|
|
|
|
/// <summary> Get point's world first control position </summary>
|
|
public Vector3 GetControlFirst(int pointIndex)
|
|
{
|
|
var count = cachedSectionInfos.Count;
|
|
|
|
if (count == 0) return curve[pointIndex].ControlFirstWorld;
|
|
|
|
if (pointIndex == 0) return curve.Closed ? cachedSectionInfos[count - 1].OriginalToControl : curve[0].ControlFirstWorld;
|
|
|
|
return cachedSectionInfos[pointIndex - 1].OriginalToControl;
|
|
}
|
|
|
|
/// <summary> Get point's world second control position </summary>
|
|
public Vector3 GetControlSecond(int pointIndex)
|
|
{
|
|
var count = cachedSectionInfos.Count;
|
|
|
|
if (count == 0) return curve[pointIndex].ControlSecondWorld;
|
|
|
|
if (pointIndex == count) return curve.Closed ? cachedSectionInfos[count - 1].OriginalFromControl : curve[pointIndex].ControlSecondWorld;
|
|
|
|
return cachedSectionInfos[pointIndex].OriginalFromControl;
|
|
}
|
|
|
|
|
|
//=========================================== Misc
|
|
/// <summary>Is given field is calculated?</summary>
|
|
/// <returns>true if field is calculated</returns>
|
|
public virtual bool IsCalculated(Field field)
|
|
{
|
|
return ((int) field & (int) config.Fields) != 0;
|
|
}
|
|
|
|
// C# standard dispose
|
|
public virtual void Dispose()
|
|
{
|
|
curve.Changed -= CurveChanged;
|
|
config.Update -= ConfigOnUpdate;
|
|
cachedSectionInfos.Clear();
|
|
poolSectionInfos.Clear();
|
|
}
|
|
|
|
|
|
//=========================================== Calculate and cache required data
|
|
|
|
/// <summary>Calculates and cache all required data for performance reason. It is an expensive operation. </summary>
|
|
public virtual void Recalculate(bool force = false)
|
|
{
|
|
if (ChangeRequested != null) ChangeRequested(this, null);
|
|
|
|
if (!force && config.ShouldUpdate != null && !config.ShouldUpdate()) return;
|
|
|
|
var currentSectionsCount = cachedSectionInfos.Count;
|
|
if (curve.PointsCount < 2)
|
|
{
|
|
cachedLength = 0;
|
|
if (currentSectionsCount > 0) cachedSectionInfos.Clear();
|
|
if (Changed != null) Changed(this, null);
|
|
return;
|
|
}
|
|
|
|
//we should at least warn about non-optimal usage of Recalculate (with more than 1 update per frame)
|
|
if (recalculatedAtFrame != Time.frameCount || Time.frameCount == createdAtFrame) recalculatedAtFrame = Time.frameCount;
|
|
else
|
|
Warning("We noticed you are updating math more than once per frame. This is not optimal. " +
|
|
"If you use curve.ImmediateChangeEvents by some reason, try to use curve.Transaction to wrap all the changes to one single event.");
|
|
|
|
|
|
var pointsCount = curve.PointsCount;
|
|
var sectionsCount = curve.Closed ? pointsCount : pointsCount - 1;
|
|
|
|
//coefficients
|
|
h = 1.0/config.Parts;
|
|
h2 = h*h;
|
|
h3 = h2*h;
|
|
|
|
//ensure all sections inited
|
|
if (currentSectionsCount != sectionsCount)
|
|
{
|
|
if (currentSectionsCount < sectionsCount)
|
|
{
|
|
//not enough sections
|
|
var toAddCount = sectionsCount - currentSectionsCount;
|
|
|
|
//try to get from the pool
|
|
var poolCount = poolSectionInfos.Count;
|
|
var toAddBeforePoolCount = toAddCount;
|
|
for (var i = poolCount - 1; i >= 0 && toAddCount > 0; i--, toAddCount--) cachedSectionInfos.Add(poolSectionInfos[i]);
|
|
|
|
//pool was used
|
|
var usedPoolCount = toAddBeforePoolCount - toAddCount;
|
|
if (usedPoolCount != 0) poolSectionInfos.RemoveRange(poolSectionInfos.Count - usedPoolCount, usedPoolCount);
|
|
|
|
// we need to create new sections (pool is empty)
|
|
if (toAddCount > 0) for (var i = 0; i < toAddCount; i++) cachedSectionInfos.Add(new SectionInfo());
|
|
}
|
|
else
|
|
{
|
|
//too many sections
|
|
var toRemoveCount = currentSectionsCount - sectionsCount;
|
|
for (var i = sectionsCount; i < currentSectionsCount; i++) poolSectionInfos.Add(cachedSectionInfos[i]);
|
|
cachedSectionInfos.RemoveRange(currentSectionsCount - toRemoveCount, toRemoveCount);
|
|
}
|
|
}
|
|
|
|
//calculate fields for each section
|
|
for (var i = 0; i < pointsCount - 1; i++) CalculateSection(i, cachedSectionInfos[i], i == 0 ? null : cachedSectionInfos[i - 1], curve[i], curve[i + 1]);
|
|
|
|
var lastSection = cachedSectionInfos[sectionsCount - 1];
|
|
if (curve.Closed)
|
|
{
|
|
CalculateSection(sectionsCount - 1, lastSection, cachedSectionInfos[sectionsCount - 2], curve[pointsCount - 1], curve[0]);
|
|
if (cacheTangent) AdjustBoundaryPointsTangents(cachedSectionInfos[0], lastSection);
|
|
}
|
|
|
|
cachedLength = lastSection.DistanceFromEndToOrigin;
|
|
|
|
if (Changed != null) Changed(this, null);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region protected methods
|
|
|
|
//print a message to console if condition is met and calls callback method
|
|
protected virtual void Warning(string message, bool condition = true, Action callback = null)
|
|
{
|
|
if (!condition || !Application.isPlaying) return;
|
|
|
|
if (!SuppressWarning) Debug.Log("BGCurve[BGCurveBaseMath] Warning! " + message + ". You can suppress all warnings by using BGCurveBaseMath.SuppressWarning=true;");
|
|
|
|
if (callback != null) callback();
|
|
}
|
|
|
|
|
|
//calculates one single section data
|
|
// Performance:
|
|
// * for example: ~100 000 section points (1000 curve's points with 100 parts each) with both controls
|
|
// ~7.8 ms for Position
|
|
// ~13.5 ms for PositionAndTangent
|
|
protected virtual void CalculateSection(int index, SectionInfo section, SectionInfo prevSection, BGCurvePointI @from, BGCurvePointI to)
|
|
{
|
|
if (section == null) section = new SectionInfo();
|
|
|
|
section.DistanceFromStartToOrigin = prevSection == null ? 0 : prevSection.DistanceFromEndToOrigin;
|
|
|
|
var straightAndOptimized = config.OptimizeStraightLines && @from.ControlType == BGCurvePoint.ControlTypeEnum.Absent && to.ControlType == BGCurvePoint.ControlTypeEnum.Absent;
|
|
var pointsCount = straightAndOptimized ? 2 : config.Parts + 1;
|
|
|
|
// do we need to recalc points?
|
|
if (Reset(section, @from, to, pointsCount))
|
|
{
|
|
if (straightAndOptimized)
|
|
{
|
|
// ===================================================== Straight section with 2 points
|
|
Resize(section.points, 2);
|
|
|
|
var startPoint = section.points[0];
|
|
var endPoint = section.points[1];
|
|
|
|
startPoint.Position = section.OriginalFrom;
|
|
endPoint.Position = section.OriginalTo;
|
|
|
|
//distance
|
|
endPoint.DistanceToSectionStart = Vector3.Distance(section.OriginalFrom, section.OriginalTo);
|
|
|
|
//tangents
|
|
if (cacheTangent) startPoint.Tangent = endPoint.Tangent = (endPoint.Position - startPoint.Position).normalized;
|
|
}
|
|
else
|
|
{
|
|
// ===================================================== Section with {parts} smaller sections
|
|
CalculateSplitSection(section, @from, to);
|
|
}
|
|
if (cacheTangent)
|
|
{
|
|
section.OriginalFirstPointTangent = section[0].Tangent;
|
|
section.OriginalLastPointTangent = section[section.PointsCount - 1].Tangent;
|
|
}
|
|
}
|
|
|
|
// we should adjust tangents for previous section's last point and first point of the current section
|
|
if (cacheTangent && prevSection != null) AdjustBoundaryPointsTangents(section, prevSection);
|
|
|
|
section.DistanceFromEndToOrigin = section.DistanceFromStartToOrigin + section[section.PointsCount - 1].DistanceToSectionStart;
|
|
}
|
|
|
|
//adjust neighbour adjacent points tangents
|
|
private void AdjustBoundaryPointsTangents(SectionInfo section, SectionInfo prevSection)
|
|
{
|
|
if (IsUseDistanceToAdjustTangents(section, prevSection))
|
|
{
|
|
var distance1 = Vector3.SqrMagnitude(section[0].Position - section[1].Position);
|
|
var distance2 = Vector3.SqrMagnitude(prevSection[prevSection.PointsCount - 1].Position - prevSection[prevSection.PointsCount - 2].Position);
|
|
var overallDistance = distance1 + distance2;
|
|
|
|
if (Math.Abs(overallDistance) < BGCurve.Epsilon) return;
|
|
|
|
var ratio = distance1/overallDistance;
|
|
var reverseRatio = 1 - ratio;
|
|
|
|
section[0].Tangent = prevSection[prevSection.PointsCount - 1].Tangent = Vector3.Normalize(new Vector3(
|
|
section.OriginalFirstPointTangent.x*ratio + prevSection.OriginalLastPointTangent.x*reverseRatio,
|
|
section.OriginalFirstPointTangent.y*ratio + prevSection.OriginalLastPointTangent.y*reverseRatio,
|
|
section.OriginalFirstPointTangent.z*ratio + prevSection.OriginalLastPointTangent.z*reverseRatio));
|
|
}
|
|
else
|
|
{
|
|
//both tangents get adjusted
|
|
section[0].Tangent = prevSection[prevSection.PointsCount - 1].Tangent = Vector3.Normalize(new Vector3(
|
|
section.OriginalFirstPointTangent.x + prevSection.OriginalLastPointTangent.x,
|
|
section.OriginalFirstPointTangent.y + prevSection.OriginalLastPointTangent.y,
|
|
section.OriginalFirstPointTangent.z + prevSection.OriginalLastPointTangent.z));
|
|
}
|
|
}
|
|
|
|
//should we use distance between approximation points for adjusting boundary tangents
|
|
protected virtual bool IsUseDistanceToAdjustTangents(SectionInfo section, SectionInfo prevSection)
|
|
{
|
|
return config.OptimizeStraightLines && section.OriginalFromControlType == BGCurvePoint.ControlTypeEnum.Absent &&
|
|
(section.OriginalToControlType == BGCurvePoint.ControlTypeEnum.Absent || prevSection.OriginalFromControlType == BGCurvePoint.ControlTypeEnum.Absent);
|
|
}
|
|
|
|
//reset section's data and returns if recalculation is needed
|
|
protected virtual bool Reset(SectionInfo section, BGCurvePointI @from, BGCurvePointI to, int pointsCount)
|
|
{
|
|
return section.Reset(@from, to, pointsCount, ignoreSectionChangedCheck);
|
|
}
|
|
|
|
|
|
// this method contains some intentional 1) copy/paste 2) methods inlining 3) operators inlining- to increase performance
|
|
|
|
/**
|
|
* Calculates one split section's data using forward differencing algorithm
|
|
*
|
|
* The technique of forward differencing allows us to compute the polynomial at t=0,
|
|
* and then incrementally calculate points by adding (and updating) the differences.
|
|
*
|
|
* Thanks to Russel Lindsay (https://gist.github.com/rlindsay/c55be560ec41144f521f)
|
|
*
|
|
* Formulas for difference between t and t+h for cubic, quadratic, and linear polynomials
|
|
* (notice that the difference between t and t+h for a cubic polynomial is quadratic ((3ak)t^2 + (3ak^2 + 2bk)t + ak^3 + bk^2 + ck),
|
|
* likewise the difference in the quadratic case is linear, and in the linear case is a constant)
|
|
*
|
|
* --------------------
|
|
* Cubic polynomial (4 points, a, b, c, d)
|
|
* C(t) = a*t^3 + b*t^2 + c*t + d
|
|
*
|
|
* Cubic polynomial difference
|
|
* C(t + h) - C(t) http://www.wolframalpha.com/input/?i=%28a*%28t+%2B+h%29%5E3+%2B+b*%28t+%2B+h%29%5E2+%2B+c*%28t+%2B+h%29+%2B+d%29+-+%28a*t%5E3+%2B+b*t%5E2+%2B+c*t+%2B+d%29+
|
|
* = ah^3 + 3ah^2 t + 3aht^2 + bh^2 + 2bht + ch
|
|
---> i) = (3ah)t^2 + (3ah^2 + 2bh)t + ah^3 + bh^2 + ch
|
|
*
|
|
* --------------------
|
|
* quadratic polynomial
|
|
* Q(t) = at^2 + bt + c
|
|
*
|
|
* quadratic polynomial difference
|
|
* Q(t + h) - Q(t) http://www.wolframalpha.com/input/?i=%28a%28t%2Bh%29%5E2+%2B+b%28t%2Bh%29+%2B+c%29+-+%28at%5E2+%2B+bt+%2B+c%29
|
|
---> ii) = (2ah)t + ah^2 + bh
|
|
*
|
|
* ------------------
|
|
* linear polynomial
|
|
* L(t) = at + b
|
|
*
|
|
* linear polynomial difference
|
|
* L(t + h) - L(t) http://www.wolframalpha.com/input/?i=%28a%28t+%2B+h%29+%2B+b%29+-+%28at+%2B+b%29
|
|
---> iii) = ah
|
|
*/
|
|
|
|
protected virtual void CalculateSplitSection(SectionInfo section, BGCurvePointI @from, BGCurvePointI to)
|
|
{
|
|
var parts = config.Parts;
|
|
Resize(section.points, parts + 1);
|
|
|
|
var points = section.points;
|
|
|
|
// all section's values
|
|
var fromPos = section.OriginalFrom;
|
|
var toPos = section.OriginalTo;
|
|
var control1 = section.OriginalFromControl;
|
|
var control2 = section.OriginalToControl;
|
|
|
|
var controlFromAbsent = @from.ControlType == BGCurvePoint.ControlTypeEnum.Absent;
|
|
var controlToAbsent = to.ControlType == BGCurvePoint.ControlTypeEnum.Absent;
|
|
var noControls = controlFromAbsent && controlToAbsent;
|
|
var bothControls = !controlFromAbsent && !controlToAbsent;
|
|
if (!noControls && !bothControls && controlFromAbsent) control1 = control2;
|
|
|
|
//snapping
|
|
var snapIsOn = curve.SnapType == BGCurve.SnapTypeEnum.Curve;
|
|
|
|
//assign first last points directly to avoid accumulation errors
|
|
var firstPoint = points[0] ?? (section.points[0] = new SectionPointInfo());
|
|
firstPoint.Position = fromPos;
|
|
var lastPoint = points[parts] ?? (section.points[parts] = new SectionPointInfo());
|
|
lastPoint.Position = toPos;
|
|
|
|
|
|
if (noControls)
|
|
{
|
|
// ======================================================== NoControls
|
|
|
|
var currentX = (double) fromPos.x;
|
|
var currentY = (double) fromPos.y;
|
|
var currentZ = (double) fromPos.z;
|
|
|
|
var stepX = ((double) toPos.x - fromPos.x)/parts;
|
|
var stepY = ((double) toPos.y - fromPos.y)/parts;
|
|
var stepZ = ((double) toPos.z - fromPos.z)/parts;
|
|
|
|
var fromToTangentWorld = Vector3.zero;
|
|
if (cacheTangent) fromToTangentWorld = (toPos - fromPos).normalized;
|
|
|
|
lastPoint.DistanceToSectionStart = Vector3.Distance(toPos, fromPos);
|
|
var onePartDistance = lastPoint.DistanceToSectionStart/parts;
|
|
|
|
//-------------------------------- Critical section
|
|
for (var i = 1; i < parts; i++)
|
|
{
|
|
var point = points[i];
|
|
|
|
currentX += stepX;
|
|
currentY += stepY;
|
|
currentZ += stepZ;
|
|
|
|
var pos = new Vector3((float) currentX, (float) currentY, (float) currentZ);
|
|
|
|
if (snapIsOn) curve.ApplySnapping(ref pos);
|
|
|
|
point.Position = pos;
|
|
|
|
//---------- tangents
|
|
if (cacheTangent)
|
|
{
|
|
if (config.UsePointPositionsToCalcTangents)
|
|
{
|
|
//-------- Calc by point's positions
|
|
var prevPoint = section[i - 1];
|
|
var prevPosition = prevPoint.Position;
|
|
var tangent = new Vector3(pos.x - prevPosition.x, pos.y - prevPosition.y, pos.z - prevPosition.z);
|
|
//normalized inlined
|
|
var marnitude = (float) Math.Sqrt((double) tangent.x*(double) tangent.x + (double) tangent.y*(double) tangent.y + (double) tangent.z*(double) tangent.z);
|
|
tangent = ((double) marnitude > 9.99999974737875E-06) ? new Vector3(tangent.x/marnitude, tangent.y/marnitude, tangent.z/marnitude) : Vector3.zero;
|
|
|
|
prevPoint.Tangent = point.Tangent = tangent;
|
|
}
|
|
else point.Tangent = fromToTangentWorld;
|
|
}
|
|
|
|
point.DistanceToSectionStart = onePartDistance*i;
|
|
}
|
|
//-------------------------------- Critical section ends
|
|
|
|
//assign last point separately to get rid of accumulation errors
|
|
if (cacheTangent) firstPoint.Tangent = lastPoint.Tangent = fromToTangentWorld;
|
|
}
|
|
else
|
|
{
|
|
//for tangents
|
|
double tX = 0, tY = 0, tZ = 0, firstTDx = 0, firstTDy = 0, firstTDz = 0;
|
|
|
|
if (bothControls)
|
|
{
|
|
// ======================================================== Both Controls (Cubic Bezier)
|
|
|
|
/* coefficients for control points (it's a standard Bezier matrix)
|
|
* you can get these numbers by converting parametric equation to standard polinomial form
|
|
*
|
|
* | 1 0 0 0| |A| | A |
|
|
* |-3 3 0 0| * |B| = | -3A + 3B |
|
|
* | 3 -6 3 0| |C| | 3A - 6B + 3C |
|
|
* |-1 3 -3 1| |D| |-A + 3B - 3C + D|
|
|
*
|
|
* http://www.wolframalpha.com/input/?i=%7B%7B1,+0,+0,+0%7D,%7B-3,+3,+0,+0%7D,+%7B3,+-6,+3,+0%7D,+%7B-1,+3,+-3,+1%7D%7D*%7B%7Bp_0%7D,+%7Bp_1%7D,+%7Bp_2%7D,+%7Bp_3%7D%7D
|
|
*
|
|
*/
|
|
// the same as in above matrix (top down)
|
|
var cx = 3*((double) control1.x - fromPos.x);
|
|
var cy = 3*((double) control1.y - fromPos.y);
|
|
var cz = 3*((double) control1.z - fromPos.z);
|
|
|
|
var bx = 3*((double) control2.x - control1.x) - cx;
|
|
var by = 3*((double) control2.y - control1.y) - cy;
|
|
var bz = 3*((double) control2.z - control1.z) - cz;
|
|
|
|
var ax = (double) toPos.x - fromPos.x - cx - bx;
|
|
var ay = (double) toPos.y - fromPos.y - cy - by;
|
|
var az = (double) toPos.z - fromPos.z - cz - bz;
|
|
|
|
var pointX = (double) fromPos.x;
|
|
var pointY = (double) fromPos.y;
|
|
var pointZ = (double) fromPos.z;
|
|
|
|
var axH3 = ax*h3;
|
|
var ax6H3 = 6*axH3;
|
|
var ayH3 = ay*h3;
|
|
var ay6H3 = 6*ayH3;
|
|
var azH3 = az*h3;
|
|
var az6H3 = 6*azH3;
|
|
|
|
var bxH2 = bx*h2;
|
|
var byH2 = by*h2;
|
|
var bzH2 = bz*h2;
|
|
|
|
/* First Difference
|
|
* the difference between t and t+h. Since the curve is cubic, difference is (from Formula i, see header)
|
|
* i) (3ah)t^2 + (3ah^2 + 2bh)t + ah^3 + bh^2 + ch
|
|
*
|
|
* Since we are calculating from the start of the curve, t = 0.
|
|
*
|
|
* http://www.wolframalpha.com/input/?i=(a*(0+%2B+h)%5E3+%2B+b*(0+%2B+h)%5E2+%2B+c*(0+%2B+h)+%2B+d)+-+(a*0%5E3+%2B+b*0%5E2+%2B+c*0+%2B+d)
|
|
* D1 = ah^3 + bh^2 + ch
|
|
*/
|
|
var firstDx = axH3 + bxH2 + cx*h;
|
|
var firstDy = ayH3 + byH2 + cy*h;
|
|
var firstDz = azH3 + bzH2 + cz*h;
|
|
|
|
/*
|
|
* Second Difference:
|
|
*
|
|
* the difference between two successive first differences. Since the form of the first difference is quadratic, the difference
|
|
* between two of them is (from Formula ii, see header )
|
|
* ii) (2ah)t + ah^2 + bh
|
|
*
|
|
* Substituting
|
|
* t = 0 to calculate the difference at the start of the curve,
|
|
* a = 3ah,
|
|
* b = 3ah^2 + 2bh (see D1 calculation, Formula i )
|
|
* we get
|
|
* D2 = (6ah^2)t + 6ah^3 + 2bh^2 http://www.wolframalpha.com/input/?i=%282*3*a*h*h%29t+%2B+3*a*h*h%5E2+%2B+%283*a*h%5E2+%2B+2*b*h%29h
|
|
* = 6ah^3 + 2bh^2
|
|
*/
|
|
var secondDx = ax6H3 + 2*bxH2;
|
|
var secondDy = ay6H3 + 2*byH2;
|
|
var secondDz = az6H3 + 2*bzH2;
|
|
|
|
/*
|
|
* Third difference:
|
|
*
|
|
* the difference between two successive second differences. since the form is linear
|
|
* iii) ah
|
|
* substituting
|
|
* a = 6ah^2 (see D2 calculation)
|
|
* D3 = 6ah^3 http://www.wolframalpha.com/input/?i=6*a*h%5E2+*+h
|
|
*
|
|
*/
|
|
var thirdDx = ax6H3;
|
|
var thirdDy = ay6H3;
|
|
var thirdDz = az6H3;
|
|
|
|
|
|
double secondTDx = 0, secondTDy = 0, secondTDz = 0;
|
|
if (cacheTangent && !config.UsePointPositionsToCalcTangents)
|
|
{
|
|
//the same thing as with positions
|
|
//parametric to polinomial standard
|
|
//3*(1-t)^2*(p1-p0) + 6*(1-t)*t*(p2-p1) + 3*t^2*(p3-p2)=(-3*p0+9*p1-9*p2+3*p3)*t^2 + (6*p0-12*p1+6*p2)*t + (3*p1-3*p0)
|
|
var tbx = 6*((double) fromPos.x - 2*control1.x + control2.x);
|
|
var tby = 6*((double) fromPos.y - 2*control1.y + control2.y);
|
|
var tbz = 6*((double) fromPos.z - 2*control1.z + control2.z);
|
|
|
|
var tax = 3*((double) -fromPos.x + 3*control1.x - 3*control2.x + toPos.x);
|
|
var tay = 3*((double) -fromPos.y + 3*control1.y - 3*control2.y + toPos.y);
|
|
var taz = 3*((double) -fromPos.z + 3*control1.z - 3*control2.z + toPos.z);
|
|
|
|
//temp
|
|
var taxH2 = tax*h2;
|
|
var tayH2 = tay*h2;
|
|
var tazH2 = taz*h2;
|
|
|
|
|
|
// ii) (2ah)t + ah^2 + bh, t=0
|
|
firstTDx = taxH2 + tbx*h;
|
|
firstTDy = tayH2 + tby*h;
|
|
firstTDz = tazH2 + tbz*h;
|
|
|
|
// iii) = ah, a=2ah
|
|
secondTDx = 2*taxH2;
|
|
secondTDy = 2*tayH2;
|
|
secondTDz = 2*tazH2;
|
|
|
|
tX = cx;
|
|
tY = cy;
|
|
tZ = cz;
|
|
|
|
//normalized inlined
|
|
var magnitude = Math.Sqrt(tX*tX + tY*tY + tZ*tZ);
|
|
firstPoint.Tangent = magnitude > 9.99999974737875E-06 ? new Vector3((float) (tX/magnitude), (float) (tY/magnitude), (float) (tZ/magnitude)) : Vector3.zero;
|
|
}
|
|
|
|
//-------------------------------- Critical section
|
|
for (var i = 1; i < parts; i++)
|
|
{
|
|
var point = points[i];
|
|
|
|
pointX += firstDx;
|
|
pointY += firstDy;
|
|
pointZ += firstDz;
|
|
|
|
firstDx += secondDx;
|
|
firstDy += secondDy;
|
|
firstDz += secondDz;
|
|
|
|
secondDx += thirdDx;
|
|
secondDy += thirdDy;
|
|
secondDz += thirdDz;
|
|
|
|
var pos = new Vector3((float) pointX, (float) pointY, (float) pointZ);
|
|
|
|
if (snapIsOn) curve.ApplySnapping(ref pos);
|
|
|
|
point.Position = pos;
|
|
|
|
if (cacheTangent)
|
|
{
|
|
if (config.UsePointPositionsToCalcTangents)
|
|
{
|
|
//-------- Calc by point's positions
|
|
var prevPoint = section[i - 1];
|
|
var prevPosition = prevPoint.Position;
|
|
var tangent = new Vector3(pos.x - prevPosition.x, pos.y - prevPosition.y, pos.z - prevPosition.z);
|
|
//normalized inlined
|
|
var marnitude = (float) Math.Sqrt((double) tangent.x*(double) tangent.x + (double) tangent.y*(double) tangent.y + (double) tangent.z*(double) tangent.z);
|
|
tangent = ((double) marnitude > 9.99999974737875E-06) ? new Vector3(tangent.x/marnitude, tangent.y/marnitude, tangent.z/marnitude) : Vector3.zero;
|
|
|
|
prevPoint.Tangent = point.Tangent = tangent;
|
|
}
|
|
else
|
|
{
|
|
tX += firstTDx;
|
|
tY += firstTDy;
|
|
tZ += firstTDz;
|
|
|
|
firstTDx += secondTDx;
|
|
firstTDy += secondTDy;
|
|
firstTDz += secondTDz;
|
|
|
|
//normalized inlined
|
|
var magnitude = Math.Sqrt(tX*tX + tY*tY + tZ*tZ);
|
|
point.Tangent = magnitude > 9.99999974737875E-06 ? new Vector3((float) (tX/magnitude), (float) (tY/magnitude), (float) (tZ/magnitude)) : Vector3.zero;
|
|
}
|
|
}
|
|
|
|
// ---------- distance to section start (Vector3.Distance inlined)
|
|
var prevPos = section[i - 1].Position;
|
|
double deltaX = pos.x - prevPos.x;
|
|
double deltaY = pos.y - prevPos.y;
|
|
double deltaZ = pos.z - prevPos.z;
|
|
point.DistanceToSectionStart = section[i - 1].DistanceToSectionStart + ((float) Math.Sqrt(deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ));
|
|
}
|
|
//-------------------------------- Critical section ends
|
|
}
|
|
else
|
|
{
|
|
// ======================================================== One Control (Quadratic Bezier)
|
|
// see comments for cubic bezier for more details
|
|
// parametric to polinomial standard form
|
|
// (1-t)^2*p0 + 2(1-t)*t*p1+t^2*p2 = (p0-2p1+p2)*t^2 + (-2p0+2p1)*t + p0
|
|
|
|
var bx = 2*((double) control1.x - fromPos.x);
|
|
var by = 2*((double) control1.y - fromPos.y);
|
|
var bz = 2*((double) control1.z - fromPos.z);
|
|
|
|
var ax = (double) fromPos.x - 2*control1.x + toPos.x;
|
|
var ay = (double) fromPos.y - 2*control1.y + toPos.y;
|
|
var az = (double) fromPos.z - 2*control1.z + toPos.z;
|
|
|
|
// ii) = (2ah)t + ah^2 + bh, t=0
|
|
var firstDx = ax*h2 + bx*h;
|
|
var firstDy = ay*h2 + by*h;
|
|
var firstDz = az*h2 + bz*h;
|
|
|
|
// iii) ah, a=2ah
|
|
var secondDx = 2*ax*h2;
|
|
var secondDy = 2*ay*h2;
|
|
var secondDz = 2*az*h2;
|
|
|
|
var pointX = (double) fromPos.x;
|
|
var pointY = (double) fromPos.y;
|
|
var pointZ = (double) fromPos.z;
|
|
|
|
if (cacheTangent && !config.UsePointPositionsToCalcTangents)
|
|
{
|
|
//the same thing as with positions
|
|
//parametric to polinomial standard
|
|
// 2*(1-t)*(p1-p0) + 2t*(p2-p1) = (2*p0-4*p1+2*p2)*t + (2*p1-2*p0)
|
|
var tax = 2*((double) fromPos.x - 2*control1.x + toPos.x);
|
|
var tay = 2*((double) fromPos.y - 2*control1.y + toPos.y);
|
|
var taz = 2*((double) fromPos.z - 2*control1.z + toPos.z);
|
|
|
|
// iii) = ah
|
|
firstTDx = tax*h;
|
|
firstTDy = tay*h;
|
|
firstTDz = taz*h;
|
|
|
|
tX = 2*((double) control1.x - fromPos.x);
|
|
tY = 2*((double) control1.y - fromPos.y);
|
|
tZ = 2*((double) control1.z - fromPos.z);
|
|
|
|
//normalized inlined
|
|
var magnitude = Math.Sqrt(tX*tX + tY*tY + tZ*tZ);
|
|
firstPoint.Tangent = magnitude > 9.99999974737875E-06 ? new Vector3((float) (tX/magnitude), (float) (tY/magnitude), (float) (tZ/magnitude)) : Vector3.zero;
|
|
}
|
|
//-------------------------------- Critical section
|
|
for (var i = 1; i < parts; i++)
|
|
{
|
|
var point = points[i];
|
|
|
|
pointX += firstDx;
|
|
pointY += firstDy;
|
|
pointZ += firstDz;
|
|
|
|
firstDx += secondDx;
|
|
firstDy += secondDy;
|
|
firstDz += secondDz;
|
|
|
|
var pos = new Vector3((float) pointX, (float) pointY, (float) pointZ);
|
|
|
|
if (snapIsOn) curve.ApplySnapping(ref pos);
|
|
|
|
point.Position = pos;
|
|
|
|
if (cacheTangent)
|
|
{
|
|
if (config.UsePointPositionsToCalcTangents)
|
|
{
|
|
//-------- Calc by point's positions
|
|
var prevPoint = section[i - 1];
|
|
var prevPosition = prevPoint.Position;
|
|
var tangent = new Vector3(pos.x - prevPosition.x, pos.y - prevPosition.y, pos.z - prevPosition.z);
|
|
//normalized inlined
|
|
var marnitude = (float) Math.Sqrt((double) tangent.x*(double) tangent.x + (double) tangent.y*(double) tangent.y + (double) tangent.z*(double) tangent.z);
|
|
tangent = ((double) marnitude > 9.99999974737875E-06) ? new Vector3(tangent.x/marnitude, tangent.y/marnitude, tangent.z/marnitude) : Vector3.zero;
|
|
|
|
prevPoint.Tangent = point.Tangent = tangent;
|
|
}
|
|
else
|
|
{
|
|
tX += firstTDx;
|
|
tY += firstTDy;
|
|
tZ += firstTDz;
|
|
|
|
//normalized inlined
|
|
var magnitude = Math.Sqrt(tX*tX + tY*tY + tZ*tZ);
|
|
point.Tangent = magnitude > 9.99999974737875E-06 ? new Vector3((float) (tX/magnitude), (float) (tY/magnitude), (float) (tZ/magnitude)) : Vector3.zero;
|
|
}
|
|
}
|
|
|
|
// ---------- distance to section start (Vector3.Distance inlined)
|
|
var prevPos = section[i - 1].Position;
|
|
double deltaX = pos.x - prevPos.x;
|
|
double deltaY = pos.y - prevPos.y;
|
|
double deltaZ = pos.z - prevPos.z;
|
|
point.DistanceToSectionStart = section[i - 1].DistanceToSectionStart + ((float) Math.Sqrt(deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ));
|
|
}
|
|
//-------------------------------- Critical section ends
|
|
}
|
|
|
|
//last point's tangent
|
|
if (cacheTangent && !config.UsePointPositionsToCalcTangents)
|
|
{
|
|
tX += firstTDx;
|
|
tY += firstTDy;
|
|
tZ += firstTDz;
|
|
var magnitude = Math.Sqrt(tX*tX + tY*tY + tZ*tZ);
|
|
lastPoint.Tangent = magnitude > 9.99999974737875E-06 ? new Vector3((float) (tX/magnitude), (float) (tY/magnitude), (float) (tZ/magnitude)) : Vector3.zero;
|
|
}
|
|
}
|
|
|
|
//last Point's distance
|
|
var beforeLastPoint = section[parts - 1];
|
|
var beforeLastPos = beforeLastPoint.Position;
|
|
var lastPointPos = lastPoint.Position;
|
|
double dX = lastPointPos.x - beforeLastPos.x;
|
|
double dY = lastPointPos.y - beforeLastPos.y;
|
|
double dZ = lastPointPos.z - beforeLastPos.z;
|
|
lastPoint.DistanceToSectionStart = beforeLastPoint.DistanceToSectionStart + ((float) Math.Sqrt(dX*dX + dY*dY + dZ*dZ));
|
|
|
|
//last point's tangent
|
|
if (cacheTangent && config.UsePointPositionsToCalcTangents)
|
|
{
|
|
//-------- Calc by point's positions
|
|
|
|
var tangent = new Vector3((float) dX, (float) dY, (float) dZ);
|
|
//normalized inlined
|
|
var marnitude = (float) Math.Sqrt((double) tangent.x*(double) tangent.x + (double) tangent.y*(double) tangent.y + (double) tangent.z*(double) tangent.z);
|
|
tangent = ((double) marnitude > 9.99999974737875E-06) ? new Vector3(tangent.x/marnitude, tangent.y/marnitude, tangent.z/marnitude) : Vector3.zero;
|
|
|
|
lastPoint.Tangent = tangent;
|
|
}
|
|
}
|
|
|
|
|
|
//search cached data and returns point's position or tangent at given distance from curve's start
|
|
// * for example (1000 sections with 100 points each, so 100 000 points) with 10000 Random searches ~7ms
|
|
protected virtual void BinarySearchByDistance(float distance, out Vector3 position, out Vector3 tangent, bool calculatePosition, bool calculateTangent)
|
|
{
|
|
var pointsCount = Curve.PointsCount;
|
|
if (pointsCount < 2 || cachedSectionInfos.Count == 0)
|
|
{
|
|
position = Vector3.zero;
|
|
tangent = Vector3.zero;
|
|
if (pointsCount == 1 && calculatePosition) position = Curve[0].PositionWorld;
|
|
}
|
|
|
|
// field was not set in the constructor, so it was not calculated and can not be accessed.
|
|
// Example, use new BGCurveBaseMath(new BGCurveBaseMath.Params(GetComponent<BGCurve>(), BGCurveBaseMath.Fields.PositionAndTangent))
|
|
// to calculate world's position and tangent
|
|
if (calculateTangent && ((int) Field.Tangent & (int) config.Fields) == 0)
|
|
throw new UnityException("Can not calculate tangent, cause it was not included in the 'fields' constructor parameter. " +
|
|
"For example, use new BGCurveBaseMath(curve, new BGCurveBaseMath.Config(" +
|
|
"BGCurveBaseMath.Fields.PositionAndTangent))" +
|
|
"to calculate world's position and tangent");
|
|
|
|
|
|
var targetSection = cachedSectionInfos[FindSectionIndexByDistance(distance)];
|
|
|
|
//after we found a section, let's search within this section
|
|
targetSection.CalcByDistance(distance - targetSection.DistanceFromStartToOrigin, out position, out tangent, calculatePosition, calculateTangent);
|
|
}
|
|
|
|
//find section by distance
|
|
protected int FindSectionIndexByDistance(float distance)
|
|
{
|
|
// ----- critical section start
|
|
int low = 0, mid = 0, high = cachedSectionInfos.Count, i = 0;
|
|
while (low < high)
|
|
{
|
|
mid = (low + high) >> 1;
|
|
var item = cachedSectionInfos[mid];
|
|
|
|
//success
|
|
if (distance >= item.DistanceFromStartToOrigin && distance <= item.DistanceFromEndToOrigin) break;
|
|
|
|
if (distance < item.DistanceFromStartToOrigin) high = mid;
|
|
else low = mid + 1;
|
|
|
|
//just in case
|
|
if (i++ > 100) throw new UnityException("Something wrong: more than 100 iterations inside BinarySearch");
|
|
}
|
|
// ----- critical section end
|
|
|
|
return mid;
|
|
}
|
|
|
|
|
|
//convert ratio to distance
|
|
protected float DistanceByRatio(float distanceRatio)
|
|
{
|
|
return GetDistance()*Mathf.Clamp01(distanceRatio);
|
|
}
|
|
|
|
//ensure distance is within proper range
|
|
protected float ClampDistance(float distance)
|
|
{
|
|
return Mathf.Clamp(distance, 0, GetDistance());
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return "Base Math for curve (" + Curve + "), sections=" + SectionsCount;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region private methods
|
|
|
|
//===============================================================================================
|
|
// Private functions
|
|
//===============================================================================================
|
|
|
|
protected void Resize(List<SectionPointInfo> points, int size)
|
|
{
|
|
var pointCount = points.Count;
|
|
if (pointCount == size) return;
|
|
|
|
//size mismatch
|
|
if (pointCount < size)
|
|
{
|
|
//we need more points
|
|
var poolIndex = poolPointInfos.Count - 1;
|
|
for (var i = pointCount; i < size; i++) points.Add(poolIndex >= 0 ? poolPointInfos[poolIndex--] : new SectionPointInfo());
|
|
|
|
//pool was used
|
|
if (poolIndex != poolPointInfos.Count - 1) poolPointInfos.RemoveRange(poolIndex + 1, poolPointInfos.Count - 1 - poolIndex);
|
|
}
|
|
else
|
|
{
|
|
//we need less points
|
|
for (var i = size; i < pointCount; i++) poolPointInfos.Add(points[i]);
|
|
|
|
points.RemoveRange(size, pointCount - size);
|
|
}
|
|
}
|
|
|
|
|
|
private void CurveChanged(object sender, BGCurveChangedArgs e)
|
|
{
|
|
ignoreSectionChangedCheck = e != null && e.ChangeType == BGCurveChangedArgs.ChangeTypeEnum.Snap;
|
|
Recalculate();
|
|
ignoreSectionChangedCheck = false;
|
|
}
|
|
|
|
private void ConfigOnUpdate(object sender, EventArgs eventArgs)
|
|
{
|
|
Recalculate(true);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper model classes
|
|
|
|
//===============================================================================================
|
|
// Helper classes
|
|
//===============================================================================================
|
|
|
|
|
|
/// <summary>params for calculations </summary>
|
|
public class Config
|
|
{
|
|
/// <summary>Which fields to calculate and cache. Do not use more fields then needed.</summary>
|
|
public Fields Fields = Fields.Position;
|
|
|
|
/// <summary>number of parts each curve's section will be devided to. Range[1, 1000]</summary>
|
|
public int Parts = 30;
|
|
|
|
/// <summary>Use points position instead of formula to calc tangents. This can increase performace.</summary>
|
|
public bool UsePointPositionsToCalcTangents;
|
|
|
|
/// <summary>Do not split straight lines during precalculation (use only 2 points per section).Tangents may suffer</summary>
|
|
public bool OptimizeStraightLines;
|
|
|
|
/// <summary>If not null it can control if math should update it's data. Updating cached data takes some resources, so this param can control updating strategy </summary>
|
|
public Func<bool> ShouldUpdate;
|
|
|
|
/// <summary>If not null math will update on this event </summary>
|
|
public event EventHandler Update;
|
|
|
|
|
|
public Config()
|
|
{
|
|
}
|
|
|
|
/// <param name="fields">Fields (from BGCurveBaseMath.Field) you want to precalculate and cache.
|
|
/// 'None' means- no precalculation and caching will occur, but as a result you will be able to use CalcPositionByT and CalcTangentByT methods only.
|
|
/// See class documentation for more info.</param>
|
|
public Config(Fields fields)
|
|
{
|
|
Fields = fields;
|
|
}
|
|
|
|
protected bool Equals(Config other)
|
|
{
|
|
return Fields == other.Fields && Parts == other.Parts && UsePointPositionsToCalcTangents == other.UsePointPositionsToCalcTangents &&
|
|
OptimizeStraightLines == other.OptimizeStraightLines;
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (ReferenceEquals(null, obj)) return false;
|
|
if (ReferenceEquals(this, obj)) return true;
|
|
if (obj.GetType() != this.GetType()) return false;
|
|
return Equals((Config) obj);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
var hashCode = (int) Fields;
|
|
hashCode = (hashCode*397) ^ Parts;
|
|
hashCode = (hashCode*397) ^ UsePointPositionsToCalcTangents.GetHashCode();
|
|
hashCode = (hashCode*397) ^ OptimizeStraightLines.GetHashCode();
|
|
return hashCode;
|
|
}
|
|
|
|
/// <summary>Fire Updated event</summary>
|
|
public void FireUpdate()
|
|
{
|
|
if (Update != null) Update(this, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>information for one single section (between 2 points) of the curve</summary>
|
|
public class SectionInfo
|
|
{
|
|
/// <summary>distance from section start to curve start</summary>
|
|
public float DistanceFromStartToOrigin;
|
|
|
|
/// <summary>distance from section end to curve start </summary>
|
|
public float DistanceFromEndToOrigin;
|
|
|
|
//all the points in this section
|
|
protected internal readonly List<SectionPointInfo> points = new List<SectionPointInfo>();
|
|
|
|
|
|
/// <summary>From location, used by calculation</summary>
|
|
public Vector3 OriginalFrom;
|
|
|
|
/// <summary>To location, used by calculation</summary>
|
|
public Vector3 OriginalTo;
|
|
|
|
/// <summary>From control type, used by calculation</summary>
|
|
public BGCurvePoint.ControlTypeEnum OriginalFromControlType;
|
|
|
|
/// <summary>To control type, used by calculation</summary>
|
|
public BGCurvePoint.ControlTypeEnum OriginalToControlType;
|
|
|
|
/// <summary>From control position, used by calculation</summary>
|
|
public Vector3 OriginalFromControl;
|
|
|
|
/// <summary>To control position, used by calculation</summary>
|
|
public Vector3 OriginalToControl;
|
|
|
|
// we need these 2 following fields, cause this section can get skipped while calculation, but adjacent sections may change,
|
|
// and this will affect adjacent points tangents
|
|
// so we need these "original tangents" to calculate final tangents in case this section will get skipped
|
|
/// <summary>First point tangent, calculated for the first time</summary>
|
|
public Vector3 OriginalFirstPointTangent;
|
|
|
|
/// <summary>Last point tangent, calculated for the first time</summary>
|
|
public Vector3 OriginalLastPointTangent;
|
|
|
|
/// <summary>Approximation points in the section</summary>
|
|
public List<SectionPointInfo> Points
|
|
{
|
|
get { return points; }
|
|
}
|
|
|
|
/// <summary>Approximation points count</summary>
|
|
public int PointsCount
|
|
{
|
|
get { return points.Count; }
|
|
}
|
|
|
|
/// <summary>Section's total distance</summary>
|
|
public float Distance
|
|
{
|
|
get { return DistanceFromEndToOrigin - DistanceFromStartToOrigin; }
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return "Section distance=(" + Distance + ")";
|
|
}
|
|
|
|
/// <summary>Get section's point by index</summary>
|
|
public SectionPointInfo this[int i]
|
|
{
|
|
get { return points[i]; }
|
|
set { points[i] = value; }
|
|
}
|
|
|
|
/// <summary>Reset section's init data and returns true, if (re)calculation is needed</summary>
|
|
protected internal bool Reset(BGCurvePointI fromPoint, BGCurvePointI toPoint, int pointsCount, bool skipCheck)
|
|
{
|
|
var newFrom = fromPoint.PositionWorld;
|
|
var newTo = toPoint.PositionWorld;
|
|
var newFromControl = fromPoint.ControlSecondWorld;
|
|
var newToControl = toPoint.ControlFirstWorld;
|
|
|
|
const float epsilon = 0.000001f;
|
|
if (
|
|
!skipCheck
|
|
&& points.Count == pointsCount
|
|
&& OriginalFromControlType == fromPoint.ControlType
|
|
&& OriginalToControlType == toPoint.ControlType
|
|
&& Vector3.SqrMagnitude(new Vector3(OriginalFrom.x - newFrom.x, OriginalFrom.y - newFrom.y, OriginalFrom.z - newFrom.z)) < epsilon
|
|
&& Vector3.SqrMagnitude(new Vector3(OriginalTo.x - newTo.x, OriginalTo.y - newTo.y, OriginalTo.z - newTo.z)) < epsilon
|
|
&& Vector3.SqrMagnitude(new Vector3(OriginalFromControl.x - newFromControl.x, OriginalFromControl.y - newFromControl.y, OriginalFromControl.z - newFromControl.z)) < epsilon
|
|
&& Vector3.SqrMagnitude(new Vector3(OriginalToControl.x - newToControl.x, OriginalToControl.y - newToControl.y, OriginalToControl.z - newToControl.z)) < epsilon
|
|
)
|
|
return false;
|
|
|
|
OriginalFrom = newFrom;
|
|
OriginalTo = newTo;
|
|
OriginalFromControlType = fromPoint.ControlType;
|
|
OriginalToControlType = toPoint.ControlType;
|
|
OriginalFromControl = newFromControl;
|
|
OriginalToControl = newToControl;
|
|
return true;
|
|
}
|
|
|
|
//copy pasted binary search algorithm
|
|
public int FindPointIndexByDistance(float distanceWithinSection)
|
|
{
|
|
var pointsCountMinusOne = points.Count - 1;
|
|
|
|
// ----- critical section start (copy paste)
|
|
int low = 0, mid = 0, high = points.Count, i = 0;
|
|
while (low < high)
|
|
{
|
|
mid = (low + high) >> 1;
|
|
var item = points[mid];
|
|
|
|
//success
|
|
if (!(distanceWithinSection < item.DistanceToSectionStart) && (mid == pointsCountMinusOne || points[mid + 1].DistanceToSectionStart >= distanceWithinSection)) break;
|
|
|
|
if (distanceWithinSection < item.DistanceToSectionStart) high = mid;
|
|
else low = mid + 1;
|
|
|
|
//just in case
|
|
if (i++ > 100) throw new UnityException("Something wrong: more than 100 iterations inside BinarySearch");
|
|
}
|
|
// ----- critical section end
|
|
return mid;
|
|
}
|
|
|
|
/// <summary>Calculates posiiton and distance by distance within this section. Note, it does not apply any checking for performance's sake </summary>
|
|
public void CalcByDistance(float distanceWithinSection, out Vector3 position, out Vector3 tangent, bool calculatePosition, bool calculateTangent)
|
|
{
|
|
position = Vector3.zero;
|
|
tangent = Vector3.zero;
|
|
|
|
if (points.Count == 2)
|
|
{
|
|
//linear
|
|
var first = points[0];
|
|
|
|
if (Math.Abs(Distance) < BGCurve.Epsilon)
|
|
{
|
|
if (calculatePosition) position = first.Position;
|
|
if (calculateTangent) tangent = first.Tangent;
|
|
}
|
|
else
|
|
{
|
|
var ratio = distanceWithinSection/Distance;
|
|
var second = points[1];
|
|
if (calculatePosition) position = Vector3.Lerp(first.Position, second.Position, ratio);
|
|
if (calculateTangent) tangent = Vector3.Lerp(first.Tangent, second.Tangent, ratio);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var pointIndex = FindPointIndexByDistance(distanceWithinSection);
|
|
var targetPoint = points[pointIndex];
|
|
|
|
//the very last point within section
|
|
if (pointIndex == points.Count - 1)
|
|
{
|
|
if (calculatePosition) position = targetPoint.Position;
|
|
if (calculateTangent) tangent = targetPoint.Tangent;
|
|
}
|
|
else
|
|
{
|
|
var nextPoint = points[pointIndex + 1];
|
|
|
|
//this is the distance between 2 points
|
|
var distanceBetweenTwoPoints = (nextPoint.DistanceToSectionStart - targetPoint.DistanceToSectionStart);
|
|
//this is smaller distance between 1st point and target distance. distanceWithinSection= the distance from section start to target distance
|
|
var distanceWithinTwoPoints = (distanceWithinSection - targetPoint.DistanceToSectionStart);
|
|
//zero division check
|
|
var ratio = Math.Abs(distanceBetweenTwoPoints) < BGCurve.Epsilon ? 0 : distanceWithinTwoPoints/distanceBetweenTwoPoints;
|
|
|
|
//lerp target's field between 1st and 2nd points using a ratio, which is based on the distance between them
|
|
if (calculatePosition) position = Vector3.Lerp(targetPoint.Position, nextPoint.Position, ratio);
|
|
if (calculateTangent) tangent = Vector3.Lerp(targetPoint.Tangent, nextPoint.Tangent, ratio);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>information for one point within a section (BGCurveBaseMath.Section)</summary>
|
|
public class SectionPointInfo
|
|
{
|
|
/// <summary>point's world position </summary>
|
|
public Vector3 Position;
|
|
|
|
|
|
/// <summary>distance from the start of the section to this point</summary>
|
|
public float DistanceToSectionStart;
|
|
|
|
/// <summary>point's world tangent</summary>
|
|
public Vector3 Tangent;
|
|
|
|
//get field's value (position or tangent)
|
|
internal Vector3 GetField(Field field)
|
|
{
|
|
Vector3 result;
|
|
switch (field)
|
|
{
|
|
case Field.Position:
|
|
result = Position;
|
|
break;
|
|
case Field.Tangent:
|
|
result = Tangent;
|
|
break;
|
|
default:
|
|
throw new UnityException("Unknown field=" + field);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//lerp field's value (position or tangent) between two points by ratio
|
|
internal Vector3 LerpTo(Field field, SectionPointInfo to, float ratio)
|
|
{
|
|
return Vector3.Lerp(GetField(field), to.GetField(field), ratio);
|
|
}
|
|
|
|
|
|
public override string ToString()
|
|
{
|
|
return "Point at (" + Position + ")";
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
#region Helper extensions for Field and Fields enums
|
|
|
|
//===============================================================================================
|
|
// Extensions
|
|
//===============================================================================================
|
|
|
|
//helper extension class for Field enum
|
|
public static class FieldExtensions
|
|
{
|
|
/// <summary> if given fieldEnum is contained in the mask </summary>
|
|
public static bool In(this BGCurveBaseMath.Field field, int mask)
|
|
{
|
|
return (field.Val() & mask) != 0;
|
|
}
|
|
|
|
/// <summary> cast to int </summary>
|
|
public static int Val(this BGCurveBaseMath.Field field)
|
|
{
|
|
return (int) field;
|
|
}
|
|
}
|
|
|
|
public static class FieldsExtensions
|
|
{
|
|
/// <summary> cast to int </summary>
|
|
public static int Val(this BGCurveBaseMath.Fields fields)
|
|
{
|
|
return (int) fields;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|