This is my attempt at creating a Union Algorithm for an arbitrary set of any number of arbitrary simple polygons. It works for both convex and concave polygons.
public static List<List<Vector2>> UnionPolygon(List<List<Vector2>> uvPolygons)
{
List<List<Vector2>> newPolygons = new List<List<Vector2>>();
#region Fix Winding Order
for (int i = 0; i < uvPolygons.Count; i++)
{
float sum = new float();
for (int j = 0; j < uvPolygons[i].Count; j++)
{
Vector3 a = uvPolygons[i][j];
Vector3 b = uvPolygons[i][(j + 1) % uvPolygons[i].Count];
sum += (b.x - a.x) * (b.y + a.y);
}
if (Mathf.Sign(sum) < 0)
uvPolygons[i].Reverse();
}
#endregion
#region Find Outside Point
Vector2 pointOutside = new Vector2();
for (int i = 0; i < uvPolygons.Count; i++)
{
for (int j = 0; j < uvPolygons[i].Count; j++)
{
if (uvPolygons[i][j].x > pointOutside.x)
{
pointOutside = uvPolygons[i][j];
}
}
}
pointOutside += new Vector2(1f, 0);
#endregion
while (uvPolygons.Count > 0)
{
List<Vector2> newPolygon = new List<Vector2>();
int P = 0; //currentPolygon
int I = 0; //currentIndex
#region Setup First Point
//Make sure starting point is not in any polygons
for (int p = 0; p < uvPolygons.Count; p++)
{
if (p != P)
{
if (!CheckIndex(I, 0, uvPolygons[0].Count))
break;
if (IsPointInPolygon(uvPolygons[0][I], uvPolygons[p], pointOutside))
{
p = 0;
I++;
continue;
}
}
}
#endregion
HashSet<int> hashedPolygons = new HashSet<int>();
while (true)
{
hashedPolygons.Add(P);
if (newPolygon.Contains(uvPolygons[P][I % uvPolygons[P].Count]))
break;
Vector2 a = uvPolygons[P][I % uvPolygons[P].Count];
Vector2 b = uvPolygons[P][(I + 1) % uvPolygons[P].Count];
newPolygon.Add(a);
bool intersected = false;
Vector3 intersection = new Vector3();
Vector3 closestIntersection = new Vector3();
float closestDistance = new float();
int tp = new int();
int ti = new int();
for (int p = 0; p < uvPolygons.Count; p++)
{
if (p != P)
{
for (int i = 0; i < uvPolygons[p].Count; i++)
{
Vector2 x = uvPolygons[p][i];
Vector2 y = uvPolygons[p][(i + 1) % uvPolygons[p].Count];
if (AreLinesIntersecting(a, b, x, y, false))
{
if (LineLineIntersection(a, (b - a).normalized, x, (y - x).normalized, out intersection))
{
if (newPolygon.Contains(intersection))
continue;
if (!intersected)
{
closestIntersection = intersection;
closestDistance = Vector3.Distance(a, intersection);
tp = p;
ti = i;
intersected = true;
}
else if (Vector3.Distance(a, intersection) < closestDistance)
{
closestIntersection = intersection;
closestDistance = Vector3.Distance(a, intersection);
tp = p;
ti = i;
}
}
}
}
}
}
if (intersected)
{
newPolygon.Add(closestIntersection);
P = tp;
I = ti;
}
I++;
}
if (hashedPolygons.Count == 1)
{
newPolygons.Add(new List<Vector2>(newPolygon));
uvPolygons.RemoveAt(0);
continue;
}
for (int i = uvPolygons.Count - 1; i >= 0; i--)
if (hashedPolygons.Contains(i))
uvPolygons.RemoveAt(i);
if (uvPolygons.Count > 0)
uvPolygons.Add(new List<Vector2>(newPolygon));
}
return newPolygons;
}
public static bool CheckIndex(int i, int min, int max)
{
if (i >= min && i < max)
return true;
else
return false;
}
public static bool AreLinesIntersecting(Vector2 p1x, Vector2 p1y, Vector2 p2x, Vector2 p2y, bool shouldIncludeEndPoints)
{
bool isIntersecting = false;
float denominator = (p2y.y - p2x.y) * (p1y.x - p1x.x) - (p2y.x - p2x.x) * (p1y.y - p1x.y);
//Make sure the denominator is > 0, if not the lines are parallel
if (denominator != 0f)
{
float u_a = ((p2y.x - p2x.x) * (p1x.y - p2x.y) - (p2y.y - p2x.y) * (p1x.x - p2x.x)) / denominator;
float u_b = ((p1y.x - p1x.x) * (p1x.y - p2x.y) - (p1y.y - p1x.y) * (p1x.x - p2x.x)) / denominator;
//Are the line segments intersecting if the end points are the same
if (shouldIncludeEndPoints)
{
//Is intersecting if u_a and u_b are between 0 and 1 or exactly 0 or 1
if (u_a >= 0f && u_a <= 1f && u_b >= 0f && u_b <= 1f)
{
isIntersecting = true;
}
}
else
{
//Is intersecting if u_a and u_b are between 0 and 1
if (u_a > 0f && u_a < 1f && u_b > 0f && u_b < 1f)
{
isIntersecting = true;
}
}
}
return isIntersecting;
}
public static bool LineLineIntersection(Vector3 linePoint1, Vector3 lineVec1, Vector3 linePoint2, Vector3 lineVec2, out Vector3 intersection)
{
Vector3 lineVec3 = linePoint2 - linePoint1;
Vector3 crossVec1and2 = Vector3.Cross(lineVec1, lineVec2);
Vector3 crossVec3and2 = Vector3.Cross(lineVec3, lineVec2);
float planarFactor = Vector3.Dot(lineVec3, crossVec1and2);
//is coplanar, and not parallel
if (Mathf.Abs(planarFactor) < 0.0001f
&& crossVec1and2.sqrMagnitude > 0.0001f)
{
float s = Vector3.Dot(crossVec3and2, crossVec1and2)
/ crossVec1and2.sqrMagnitude;
intersection = linePoint1 + (lineVec1 * s);
return true;
}
else
{
intersection = Vector3.zero;
return false;
}
}
public static bool IsPointInPolygon(Vector2 point, List<Vector2> polygonPoints, Vector2 pointOutside)
{
int numIntersections = 0;
for (int j = 0; j < polygonPoints.Count; j++)
{
Vector2 uv1 = polygonPoints[j];
Vector2 uv2 = polygonPoints[(j + 1) % polygonPoints.Count];
if (AreLinesIntersecting(point, pointOutside, uv1, uv2, true))
numIntersections++;
}
if (numIntersections == 0 || numIntersections % 2 == 0)
return false;
else
return true;
}
Briefly on how this algorithm works is it follows a polygon through its winding order until it detects an intersection. Upon intersection, it jumps to the intersected polygon. Repeat until it has made its way around to the beginning.
Setup requirements for this to work correctly:
- The Starting Index Must Be Outside All Other Polygons
- All Polygons Must Share Winding Order
- All Polygons Must Be Simple (i.e. not open or self intersecting)
Finally, if two polygons share an intersecting edge, the algorithm will "jump over" the second polygon. (as seen in figure 1) So it keeps track of the number of starting polygons and will repeat until all are accounted for.
It has a superficial resemblance to the Weiler Atherton algorithm