You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

656 lines
32 KiB

using System;
using UnityEngine;
namespace BansheeGz.BGSpline.Curve
{
/// <summary>
/// Closest point related calculations
/// </summary>
//this class contains some intentional copy/paste for the sake of performance
public class BGCurveCalculatorClosestPoint
{
// transitions ("safe" or not)
private static readonly int[] TransitionsForPartitions;
// math to use for calculation
private readonly BGCurveBaseMath math;
//reusable arrays to reduce GC
private bool[] excludedSections;
private float[] minSectionDistances;
static BGCurveCalculatorClosestPoint()
{
// Init transitions data
//25=inside
const int nearPlane = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5) + (1 << 6) + (1 << 7) + (1 << 24);
const int farPlane = (1 << 16) + (1 << 17) + (1 << 18) + (1 << 19) + (1 << 20) + (1 << 21) + (1 << 22) + (1 << 23) + (1 << 26);
const int bottomPlane = (1 << 0) + (1 << 6) + (1 << 7) + (1 << 8) + (1 << 14) + (1 << 15) + (1 << 16) + (1 << 22) + (1 << 23);
const int topPlane = (1 << 2) + (1 << 3) + (1 << 4) + (1 << 10) + (1 << 11) + (1 << 12) + (1 << 18) + (1 << 19) + (1 << 20);
const int leftPlane = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 8) + (1 << 9) + (1 << 10) + (1 << 16) + (1 << 17) + (1 << 18);
const int rightPlane = (1 << 4) + (1 << 5) + (1 << 6) + (1 << 12) + (1 << 13) + (1 << 14) + (1 << 20) + (1 << 21) + (1 << 22);
//transition from->to which is considered safe in terms of mininum closest point search
//total 27 partitions
TransitionsForPartitions = new[]
{
//z==0 (near)
/*0*/ nearPlane | leftPlane | bottomPlane,
/*1*/ nearPlane | leftPlane,
/*2*/ nearPlane | leftPlane | topPlane,
/*3*/ nearPlane | topPlane,
/*4*/ nearPlane | rightPlane | topPlane,
/*5*/ nearPlane | rightPlane,
/*6*/ nearPlane | rightPlane | bottomPlane,
/*7*/ nearPlane | bottomPlane,
//z==1 (middle)
/*8*/ leftPlane | bottomPlane,
/*9*/ leftPlane,
/*10*/ leftPlane | topPlane,
/*11*/ topPlane,
/*12*/ rightPlane | topPlane,
/*13*/ rightPlane,
/*14*/ rightPlane | bottomPlane,
/*15*/ bottomPlane,
//z==2 (far)
/*16*/ farPlane | leftPlane | bottomPlane,
/*17*/ farPlane | leftPlane,
/*18*/ farPlane | leftPlane | topPlane,
/*19*/ farPlane | topPlane,
/*20*/ farPlane | rightPlane | topPlane,
/*21*/ farPlane | rightPlane,
/*22*/ farPlane | rightPlane | bottomPlane,
/*23*/ farPlane | bottomPlane,
//3 left partitions in the middle
/*24*/ nearPlane,
/*25*/ 0, //inside
/*26*/ farPlane,
};
}
public BGCurveCalculatorClosestPoint(BGCurveBaseMath math)
{
this.math = math;
}
//this method contains some intentional copy/paste for the sake of performance
public Vector3 CalcPositionByClosestPoint(Vector3 targetPoint, out float distance, out Vector3 tangent, bool skipSectionsOptimization = false, bool skipPointsOptimization = false)
{
/*
minimalistic thing to do- iterate all lines and use closest point to line formula.
2 ideas for optimization:
1) Iterate all sections first, calculate min bounding box and cut off those sections far away from it
2) Iterate points, and calculate min sphere radius to contain the result point. Then partition the space by the bounding box around this sphere, and cut off
all points, that can not be contained in the box or intersect this box with a line;
*/
var sections = math.SectionInfos;
var sectionsCount = sections.Count;
if (sectionsCount == 0)
{
distance = 0;
tangent = Vector3.zero;
return math.Curve.PointsCount == 1 ? math.Curve[0].PositionWorld : Vector3.zero;
}
//==========================================================================================================================
// Section Bbox partitioning (4 passes total)
//==========================================================================================================================
var sectionsOptimizationOn = !skipSectionsOptimization && math.Configuration.Parts > 8;
var minSectionIndex = 0;
var minSectionMaxDistance = float.MaxValue;
if (sectionsOptimizationOn)
{
//----------------------- Section Bbox Partitioning 1st pass (calculate min AABB (Axis Aligned Bounding Box) by square distance between point and section's AABB)
Array.Resize(ref excludedSections, sectionsCount);
Array.Resize(ref minSectionDistances, sectionsCount);
var minBounds = new Bounds();
var minAabbDistance = float.MaxValue;
for (var i = 0; i < sectionsCount; i++)
{
var section = sections[i];
var fromAbsent = section.OriginalFromControlType == BGCurvePoint.ControlTypeEnum.Absent;
var toAbsent = section.OriginalToControlType == BGCurvePoint.ControlTypeEnum.Absent;
var originalFrom = section.OriginalFrom;
var originalTo = section.OriginalTo;
var originalToControl = section.OriginalToControl;
var fromToMinX = originalFrom.x > originalTo.x ? originalTo.x : originalFrom.x;
var fromToMinY = originalFrom.y > originalTo.y ? originalTo.y : originalFrom.y;
var fromToMinZ = originalFrom.z > originalTo.z ? originalTo.z : originalFrom.z;
var fromToMaxX = originalFrom.x < originalTo.x ? originalTo.x : originalFrom.x;
var fromToMaxY = originalFrom.y < originalTo.y ? originalTo.y : originalFrom.y;
var fromToMaxZ = originalFrom.z < originalTo.z ? originalTo.z : originalFrom.z;
float minX, minY, minZ, maxX, maxY, maxZ;
if (fromAbsent)
{
if (toAbsent)
{
//No controls
minX = fromToMinX;
minY = fromToMinY;
minZ = fromToMinZ;
maxX = fromToMaxX;
maxY = fromToMaxY;
maxZ = fromToMaxZ;
}
else
{
//To Control Present
minX = fromToMinX > originalToControl.x ? originalToControl.x : fromToMinX;
minY = fromToMinY > originalToControl.y ? originalToControl.y : fromToMinY;
minZ = fromToMinZ > originalToControl.z ? originalToControl.z : fromToMinZ;
maxX = fromToMaxX < originalToControl.x ? originalToControl.x : fromToMaxX;
maxY = fromToMaxY < originalToControl.y ? originalToControl.y : fromToMaxY;
maxZ = fromToMaxZ < originalToControl.z ? originalToControl.z : fromToMaxZ;
}
}
else
{
var originalFromControl = section.OriginalFromControl;
if (toAbsent)
{
//From Control Present
minX = fromToMinX > originalFromControl.x ? originalFromControl.x : fromToMinX;
minY = fromToMinY > originalFromControl.y ? originalFromControl.y : fromToMinY;
minZ = fromToMinZ > originalFromControl.z ? originalFromControl.z : fromToMinZ;
maxX = fromToMaxX < originalFromControl.x ? originalFromControl.x : fromToMaxX;
maxY = fromToMaxY < originalFromControl.y ? originalFromControl.y : fromToMaxY;
maxZ = fromToMaxZ < originalFromControl.z ? originalFromControl.z : fromToMaxZ;
}
else
{
//Both Controls
var fromToControlToMinX = fromToMinX > originalToControl.x ? originalToControl.x : fromToMinX;
var fromToControlToMinY = fromToMinY > originalToControl.y ? originalToControl.y : fromToMinY;
var fromToControlToMinZ = fromToMinZ > originalToControl.z ? originalToControl.z : fromToMinZ;
var fromToControlToMaxX = fromToMaxX < originalToControl.x ? originalToControl.x : fromToMaxX;
var fromToControlToMaxY = fromToMaxY < originalToControl.y ? originalToControl.y : fromToMaxY;
var fromToControlToMaxZ = fromToMaxZ < originalToControl.z ? originalToControl.z : fromToMaxZ;
minX = fromToControlToMinX > originalFromControl.x ? originalFromControl.x : fromToControlToMinX;
minY = fromToControlToMinY > originalFromControl.y ? originalFromControl.y : fromToControlToMinY;
minZ = fromToControlToMinZ > originalFromControl.z ? originalFromControl.z : fromToControlToMinZ;
maxX = fromToControlToMaxX < originalFromControl.x ? originalFromControl.x : fromToControlToMaxX;
maxY = fromToControlToMaxY < originalFromControl.y ? originalFromControl.y : fromToControlToMaxY;
maxZ = fromToControlToMaxZ < originalFromControl.z ? originalFromControl.z : fromToControlToMaxZ;
}
}
var deltaX = maxX - minX;
var deltaY = maxY - minY;
var deltaZ = maxZ - minZ;
var extents = new Vector3(deltaX*.5f, deltaY*.5f, deltaZ*.5f);
minBounds.extents = extents;
minBounds.center = new Vector3(minX + extents.x, minY + extents.y, minZ + extents.z);
var sqrDistance = minBounds.SqrDistance(targetPoint);
excludedSections[i] = false;
minSectionDistances[i] = sqrDistance;
if (!(minAabbDistance > sqrDistance)) continue;
minAabbDistance = sqrDistance;
minSectionIndex = i;
}
//calc max (between points and their related controls)
var minSection = sections[minSectionIndex];
minSectionMaxDistance = MaxDistance(minSection, targetPoint) - BGCurve.Epsilon;
//----------------------- Section Bbox Partitioning 2nd pass (1) cut off sections, which min distance to point more than max distance to min sections AABB and 2) adjusting min section )
var sectionsLeft = 0;
var maxDistanceChanged = false;
var start = minSectionIndex;
var end = Mathf.Max(start + 1, sectionsCount - start);
for (var i = 1; i < end; i++)
{
//the reason why we iterate simulteneously- to decrease chances of MaxDistance expensive calls
//to end
var j = start + i;
if (j < sectionsCount)
{
var section = sections[j];
if (minSectionDistances[j] > minSectionMaxDistance) excludedSections[j] = true;
else
{
var sectionDistance = MaxDistance(section, targetPoint);
if (minSectionMaxDistance > sectionDistance)
{
minSectionMaxDistance = sectionDistance;
maxDistanceChanged = true;
}
sectionsLeft++;
}
}
//to zero (copy/pasted)
j = start - i;
if (j >= 0)
{
var section = sections[j];
if (minSectionDistances[j] > minSectionMaxDistance) excludedSections[j] = true;
else
{
var sectionDistance = MaxDistance(section, targetPoint);
if (minSectionMaxDistance > sectionDistance)
{
minSectionMaxDistance = sectionDistance;
maxDistanceChanged = true;
}
sectionsLeft++;
}
}
}
//----------------------- Section Bbox Partitioning 3rd pass (since minSectionMaxDistance may be changed in 2nd pass, we need to iterate sections and filter them again)
if (maxDistanceChanged && sectionsLeft > 1)
for (var i = 0; i < sectionsCount; i++) if (!excludedSections[i] && minSectionDistances[i] > minSectionMaxDistance) excludedSections[i] = true;
}
//==========================================================================================================================
// Lines AABB partitioning
//==========================================================================================================================
var pointsOptimizationOn = !skipPointsOptimization;
var closestLineMaxDistance = float.MaxValue;
var fromDistance = float.MaxValue;
var fromLess = true;
var resetFrom = true;
if (pointsOptimizationOn)
{
for (var i = 0; i < sectionsCount; i++)
{
if (sectionsOptimizationOn && excludedSections[i])
{
resetFrom = true;
continue;
}
var section = sections[i];
var points = section.Points;
var pointsCount = points.Count;
if (resetFrom)
{
resetFrom = false;
fromDistance = Vector3.SqrMagnitude(section[0].Position - targetPoint);
fromLess = fromDistance <= closestLineMaxDistance;
}
for (var j = 1; j < pointsCount; j++)
{
var to = points[j];
var toPos = to.Position;
//sqr magnitude inlined
var deltaX = toPos.x - targetPoint.x;
var deltaY = toPos.y - targetPoint.y;
var deltaZ = toPos.z - targetPoint.z;
var toDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
var toLess = toDistance <= closestLineMaxDistance;
if (fromLess && toLess) closestLineMaxDistance = fromDistance > toDistance ? fromDistance : toDistance;
fromLess = toLess;
fromDistance = toDistance;
}
}
//====================================================================== Section Bbox partitioning (again)
//----------------------- Section Bbox Partitioning 4th pass (cause closestLineMaxDistance may be smaller than minSectionMaxDistance we used before)
if (sectionsOptimizationOn && closestLineMaxDistance < minSectionMaxDistance)
for (var i = 0; i < sectionsCount; i++) if (!excludedSections[i] && minSectionDistances[i] > closestLineMaxDistance) excludedSections[i] = true;
}
//================================================================================================
// Final Pass
//================================================================================================
var firstPoint = sections[0][0];
var fromPoint = firstPoint;
var result = fromPoint.Position;
var minDistance = Vector3.SqrMagnitude(targetPoint - fromPoint.Position);
var sectionIndex = 0;
var pointIndex = 0;
var resultLerp = 0f;
var fromPartition = -1;
var resetFromPoint = true;
Vector3 min = Vector3.zero, max = Vector3.zero;
if (pointsOptimizationOn)
{
//bounding Box over sphere (for points optimization)
var radius = (float) Math.Sqrt(closestLineMaxDistance);
min = new Vector3(targetPoint.x - radius, targetPoint.y - radius, targetPoint.z - radius);
max = new Vector3(targetPoint.x + radius, targetPoint.y + radius, targetPoint.z + radius);
}
for (var i = 0; i < sectionsCount; i++)
{
if (sectionsOptimizationOn && excludedSections[i])
{
resetFromPoint = true;
continue;
}
var section = sections[i];
var points = section.Points;
if (resetFromPoint)
{
resetFromPoint = false;
fromPoint = section[0];
if (pointsOptimizationOn)
{
//------------------------------------------ points optimization
//xy quadrant clockwise (0-7)
var fromPos = fromPoint.Position;
var zSet = false;
if (fromPos.x < min.x)
{
if (fromPos.y < min.y) fromPartition = 0;
else if (fromPos.y > max.y) fromPartition = 2;
else fromPartition = 1;
}
else if (fromPos.x > max.x)
{
if (fromPos.y < min.y) fromPartition = 6;
else if (fromPos.y > max.y) fromPartition = 4;
else fromPartition = 5;
}
else
{
if (fromPos.y < min.y) fromPartition = 7;
else if (fromPos.y > max.y) fromPartition = 3;
else
{
if (fromPos.z > max.z) fromPartition = 26;
else if (fromPos.z < min.z) fromPartition = 24;
else fromPartition = 25;
zSet = true;
}
}
//z shift
if (!zSet)
{
if (fromPos.z > max.z) fromPartition = fromPartition | 16;
else if (fromPos.z > min.z) fromPartition = fromPartition | 8;
}
}
}
var pointsCount = points.Count;
for (var j = 1; j < pointsCount; j++)
{
var toPoint = points[j];
var toPointPos = toPoint.Position;
var excludedByOptimization = false;
var toPartition = -1;
if (pointsOptimizationOn)
{
//------------------------------------------ points optimization
// copy/pasted
var zSet = false;
if (toPointPos.x < min.x)
{
if (toPointPos.y < min.y) toPartition = 0;
else if (toPointPos.y > max.y) toPartition = 2;
else toPartition = 1;
}
else if (toPointPos.x > max.x)
{
if (toPointPos.y < min.y) toPartition = 6;
else if (toPointPos.y > max.y) toPartition = 4;
else toPartition = 5;
}
else
{
if (toPointPos.y < min.y) toPartition = 7;
else if (toPointPos.y > max.y) toPartition = 3;
else
{
//center
if (toPointPos.z > max.z) toPartition = 26;
else if (toPointPos.z < min.z) toPartition = 24;
else toPartition = 25; //inside
zSet = true;
}
}
//z shift
if (!zSet)
{
if (toPointPos.z > max.z) toPartition = toPartition | 16;
else if (toPointPos.z > min.z) toPartition = toPartition | 8;
}
excludedByOptimization = (TransitionsForPartitions[fromPartition] & (1 << toPartition)) != 0;
}
if (!excludedByOptimization)
{
float ratio;
var gettingCloser = false;
//============================================================================= Get closest point on line formula: Line(from->to), Point(targetPoint)
// closest on line
double pointX;
double pointY;
double pointZ;
var fromPos = fromPoint.Position;
var toPos = toPoint.Position;
var fromTargetX = (double) targetPoint.x - (double) fromPos.x;
var fromTargetY = (double) targetPoint.y - (double) fromPos.y;
var fromTargetZ = (double) targetPoint.z - (double) fromPos.z;
var fromToX = (double) toPos.x - (double) fromPos.x;
var fromToY = (double) toPos.y - (double) fromPos.y;
var fromToZ = (double) toPos.z - (double) fromPos.z;
//sqr magnitude inlined
var fromToSquaredDistance = (float) (fromToX*fromToX + fromToY*fromToY + fromToZ*fromToZ);
if (Math.Abs(fromToSquaredDistance) < BGCurve.Epsilon)
{
gettingCloser = true;
ratio = 1;
pointX = toPos.x;
pointY = toPos.y;
pointZ = toPos.z;
}
else
{
// dot inlined
var dot = (float) (fromTargetX*fromToX + fromTargetY*fromToY + fromTargetZ*fromToZ);
if (dot < 0)
{
ratio = 0;
pointX = fromPos.x;
pointY = fromPos.y;
pointZ = fromPos.z;
}
else if (dot > fromToSquaredDistance)
{
gettingCloser = true;
ratio = 1;
pointX = toPos.x;
pointY = toPos.y;
pointZ = toPos.z;
}
else
{
var dotRatio = dot / fromToSquaredDistance;
ratio = dotRatio;
pointX = fromPos.x + fromToX*dotRatio;
pointY = fromPos.y + fromToY*dotRatio;
pointZ = fromPos.z + fromToZ*dotRatio;
}
}
//------------------------------------------------------ Compare with min distance
//sqr magnitude inlined
var deltaX = (double) targetPoint.x - (double) pointX;
var deltaY = (double) targetPoint.y - (double) pointY;
var deltaZ = (double) targetPoint.z - (double) pointZ;
var sqrMagnitude = (float) (deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ);
if (sqrMagnitude < minDistance)
{
minDistance = sqrMagnitude;
result.x = (float) pointX;
result.y = (float) pointY;
result.z = (float) pointZ;
sectionIndex = i;
if (gettingCloser)
{
//if we are getting closer-> simply move the pointer to current point
if (j == pointsCount - 1 && i < sectionsCount - 1)
{
//if it's the last point in the current section-> move pointer to 1st point of the next section (to calc distances correctly)
sectionIndex = i + 1;
pointIndex = 0;
}
else
{
pointIndex = j;
}
resultLerp = 0;
}
else
{
//current closest point somewhere on this line (resultLerp show how far from [j - 1] to [j])
pointIndex = j - 1;
resultLerp = ratio;
}
}
}
fromPoint = toPoint;
fromPartition = toPartition;
}
}
//================================================================================================
// Final result
//================================================================================================
var resultSection = sections[sectionIndex];
if (resultLerp > 0 && resultLerp < 1 && pointIndex < resultSection.PointsCount - 1)
{
var lerpFrom = resultSection[pointIndex];
var lerpTo = resultSection[pointIndex + 1];
tangent = Vector3.Lerp(lerpFrom.Tangent, lerpTo.Tangent, resultLerp);
distance = sections[sectionIndex].DistanceFromStartToOrigin + Mathf.Lerp(lerpFrom.DistanceToSectionStart, lerpTo.DistanceToSectionStart, resultLerp);
}
else
{
var resultPoint = sections[sectionIndex][pointIndex];
tangent = resultPoint.Tangent;
distance = sections[sectionIndex].DistanceFromStartToOrigin + resultPoint.DistanceToSectionStart;
}
return result;
}
//max distance between a point and a section
private static float MaxDistance(BGCurveBaseMath.SectionInfo section, Vector3 position)
{
// var fromDistance = Vector3.SqrMagnitude(section.OriginalFrom - position);
var deltaX = section.OriginalFrom.x - position.x;
var deltaY = section.OriginalFrom.y - position.y;
var deltaZ = section.OriginalFrom.z - position.z;
var fromDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
// var toDistance = Vector3.SqrMagnitude(section.OriginalTo - position);
deltaX = section.OriginalTo.x - position.x;
deltaY = section.OriginalTo.y - position.y;
deltaZ = section.OriginalTo.z - position.z;
var toDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
var fromControlAbsent = section.OriginalFromControlType == BGCurvePoint.ControlTypeEnum.Absent;
var toControlAbsent = section.OriginalToControlType == BGCurvePoint.ControlTypeEnum.Absent;
float maxDistance;
if (fromControlAbsent && toControlAbsent)
{
maxDistance = Mathf.Max(fromDistance, toDistance);
}
else
{
if (fromControlAbsent)
{
// var toControlDistance = Vector3.SqrMagnitude(section.OriginalToControl - position);
deltaX = section.OriginalToControl.x - position.x;
deltaY = section.OriginalToControl.y - position.y;
deltaZ = section.OriginalToControl.z - position.z;
var toControlDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
maxDistance = Mathf.Max(Mathf.Max(fromDistance, toDistance), toControlDistance);
}
else if (toControlAbsent)
{
// var fromControlDistance = Vector3.SqrMagnitude(section.OriginalFromControl - position);
deltaX = section.OriginalFromControl.x - position.x;
deltaY = section.OriginalFromControl.y - position.y;
deltaZ = section.OriginalFromControl.z - position.z;
var fromControlDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
maxDistance = Mathf.Max(Mathf.Max(fromDistance, toDistance), fromControlDistance);
}
else
{
// var fromControlDistance = Vector3.SqrMagnitude(section.OriginalFromControl - position);
deltaX = section.OriginalFromControl.x - position.x;
deltaY = section.OriginalFromControl.y - position.y;
deltaZ = section.OriginalFromControl.z - position.z;
var fromControlDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
// var toControlDistance = Vector3.SqrMagnitude(section.OriginalToControl - position);
deltaX = section.OriginalToControl.x - position.x;
deltaY = section.OriginalToControl.y - position.y;
deltaZ = section.OriginalToControl.z - position.z;
var toControlDistance = deltaX*deltaX + deltaY*deltaY + deltaZ*deltaZ;
maxDistance = Mathf.Max(Mathf.Max(Mathf.Max(fromDistance, toDistance), fromControlDistance), toControlDistance);
}
}
return maxDistance;
}
}
}