public static float4 Tessellate(Allocator allocator, NativeArray <float2> points, NativeArray <int2> edges, ref NativeArray <float2> outVertices, ref int outVertexCount, ref NativeArray <int> outIndices, ref int outIndexCount, ref NativeArray <int2> outEdges, ref int outEdgeCount) { // Inputs are garbage, just early out. float4 ret = float4.zero; outEdgeCount = 0; outIndexCount = 0; outVertexCount = 0; if (points.Length < 3 || points.Length >= kMaxVertexCount) { return(ret); } // Ensure inputs form a proper PlanarGraph. bool validGraph = false, handleEdgeCase = false; int pgEdgeCount = 0, pgPointCount = 0; NativeArray <int2> pgEdges = new NativeArray <int2>(kMaxEdgeCount, allocator); NativeArray <float2> pgPoints = new NativeArray <float2>(kMaxVertexCount, allocator); // Valid Edges and Paths, correct the Planar Graph. If invalid create a simple convex hull rect. if (0 != edges.Length) { validGraph = PlanarGraph.Validate(allocator, points, points.Length, edges, edges.Length, ref pgPoints, ref pgPointCount, ref pgEdges, ref pgEdgeCount); } // Fallbacks are now handled by the Higher level packages. Enable if UTess needs to handle it. // #if UTESS_QUAD_FALLBACK // if (!validGraph) // { // pgPointCount = 0; // handleEdgeCase = true; // ModuleHandle.Copy(points, pgPoints, points.Length); // GraphConditioner(points, ref pgPoints, ref pgPointCount, ref pgEdges, ref pgEdgeCount, false); // } // #else // If its not a valid Graph simply return back input Data without triangulation instead of going through UTess (pointless wasted cpu cycles). if (!validGraph) { outEdgeCount = edges.Length; outVertexCount = points.Length; ModuleHandle.Copy(edges, outEdges, edges.Length); ModuleHandle.Copy(points, outVertices, points.Length); } // Do a proper Delaunay Triangulation if Inputs are valid. if (pgPointCount > 2 && pgEdgeCount > 2) { int tsIndexCount = 0, tsVertexCount = 0; NativeArray <int> tsIndices = new NativeArray <int>(kMaxIndexCount, allocator); NativeArray <float2> tsVertices = new NativeArray <float2>(kMaxVertexCount, allocator); validGraph = Tessellator.Tessellate(allocator, pgPoints, pgPointCount, pgEdges, pgEdgeCount, ref tsVertices, ref tsVertexCount, ref tsIndices, ref tsIndexCount); if (validGraph) { // Copy Out TransferOutput(pgEdges, pgEdgeCount, ref outEdges, ref outEdgeCount, tsIndices, tsIndexCount, ref outIndices, ref outIndexCount, tsVertices, tsVertexCount, ref outVertices, ref outVertexCount); if (handleEdgeCase == true) { outEdgeCount = 0; } } tsVertices.Dispose(); tsIndices.Dispose(); } // Dispose Temp Memory. pgPoints.Dispose(); pgEdges.Dispose(); return(ret); }
internal static bool CutEdges(ref NativeArray <double2> points, ref int pointCount, ref NativeArray <int2> edges, ref int edgeCount, ref NativeArray <int2> tJunctions, ref int tJunctionCount, NativeArray <int2> intersections, NativeArray <double2> intersects, int intersectionCount) { for (int i = 0; i < intersectionCount; ++i) { var intr = intersections[i]; var e = intr.x; var f = intr.y; int2 j1 = int2.zero; j1.x = e; j1.y = pointCount; tJunctions[tJunctionCount++] = j1; int2 j2 = int2.zero; j2.x = f; j2.y = pointCount; tJunctions[tJunctionCount++] = j2; // Until we ensure Outline is generated properly, we need this useless Check every correction. if (pointCount >= points.Length) { return(false); } points[pointCount++] = intersects[i]; } unsafe { ModuleHandle.InsertionSort <int2, TessJunctionCompare>( NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(tJunctions), 0, tJunctionCount - 1, new TessJunctionCompare()); } // Split edges along junctions for (int i = tJunctionCount - 1; i >= 0; --i) { var tJunction = tJunctions[i]; var e = tJunction.x; var edge = edges[e]; var s = edge.x; var t = edge.y; // Check if edge is not lexicographically sorted var a = points[s]; var b = points[t]; if (((a.x - b.x) < 0) || (a.x == b.x && (a.y - b.y) < 0)) { var tmp = s; s = t; t = tmp; } // Split leading edge edge.x = s; var last = edge.y = tJunction.y; edges[e] = edge; // If we are grouping edges by color, remember to track data // Split other edges while (i > 0 && tJunctions[i - 1].x == e) { var next = tJunctions[--i].y; int2 te = new int2(); te.x = last; te.y = next; edges[edgeCount++] = te; last = next; } int2 le = new int2(); le.x = last; le.y = t; edges[edgeCount++] = le; } return(true); }
// Validate the Input Points ane Edges. internal static bool Validate(Allocator allocator, NativeArray <float2> inputPoints, int pointCount, NativeArray <int2> inputEdges, int edgeCount, ref NativeArray <float2> outputPoints, ref int outputPointCount, ref NativeArray <int2> outputEdges, ref int outputEdgeCount) { var protectLoop = edgeCount; var requiresFix = true; var validGraph = false; // Processing Arrays. NativeArray <int2> edges = new NativeArray <int2>(ModuleHandle.kMaxEdgeCount, allocator); NativeArray <double2> points = new NativeArray <double2>(ModuleHandle.kMaxVertexCount, allocator); NativeArray <int2> tJunctions = new NativeArray <int2>(ModuleHandle.kMaxEdgeCount, allocator); NativeArray <int2> edgeIntersections = new NativeArray <int2>(ModuleHandle.kMaxEdgeCount, allocator); NativeArray <int> duplicates = new NativeArray <int>(ModuleHandle.kMaxVertexCount, allocator); NativeArray <double2> intersects = new NativeArray <double2>(ModuleHandle.kMaxEdgeCount, allocator); // Initialize. for (int i = 0; i < pointCount; ++i) { points[i] = inputPoints[i]; } ModuleHandle.Copy(inputEdges, edges, edgeCount); // Pre-clear duplicates, otherwise the following will simply fail. RemoveDuplicateEdges(ref edges, ref edgeCount, duplicates, 0); // While PSG is clean. while (requiresFix && --protectLoop > 0) { // Edge Edge Intersection. int intersectionCount = 0; validGraph = CalculateEdgeIntersections(edges, edgeCount, points, pointCount, ref edgeIntersections, ref intersects, ref intersectionCount); if (!validGraph) { break; } // Edge Point Intersection. T-Junction. int tJunctionCount = 0; validGraph = CalculateTJunctions(edges, edgeCount, points, pointCount, tJunctions, ref tJunctionCount); if (!validGraph) { break; } // Cut Overlapping Edges. validGraph = CutEdges(ref points, ref pointCount, ref edges, ref edgeCount, ref tJunctions, ref tJunctionCount, edgeIntersections, intersects, intersectionCount); if (!validGraph) { break; } // Remove Duplicate Points. int duplicateCount = 0; RemoveDuplicatePoints(ref points, ref pointCount, ref duplicates, ref duplicateCount, allocator); RemoveDuplicateEdges(ref edges, ref edgeCount, duplicates, duplicateCount); requiresFix = intersectionCount != 0 || tJunctionCount != 0; } if (validGraph) { // Finalize Output. outputEdgeCount = edgeCount; outputPointCount = pointCount; ModuleHandle.Copy(edges, outputEdges, edgeCount); for (int i = 0; i < pointCount; ++i) { outputPoints[i] = new float2((float)points[i].x, (float)points[i].y); } } edges.Dispose(); points.Dispose(); intersects.Dispose(); duplicates.Dispose(); tJunctions.Dispose(); edgeIntersections.Dispose(); return(validGraph && protectLoop > 0); }
internal static bool Condition(Allocator allocator, float factorArea, float targetArea, ref NativeArray <float2> pgPoints, ref int pgPointCount, ref NativeArray <int2> pgEdges, ref int pgEdgeCount, ref NativeArray <float2> vertices, ref int vertexCount, ref NativeArray <int> indices, ref int indexCount, ref float maxArea) { // Process Triangles. maxArea = 0.0f; var minArea = 0.0f; var avgArea = 0.0f; var refined = false; var validGraph = true; // Temporary Stuffs. int triangleCount = 0, invalidTriangle = -1, inputPointCount = pgPointCount; var encroach = new NativeArray <UEncroachingSegment>(ModuleHandle.kMaxEdgeCount, allocator); var triangles = new NativeArray <UTriangle>(ModuleHandle.kMaxTriangleCount, allocator); ModuleHandle.BuildTriangles(vertices, vertexCount, indices, indexCount, ref triangles, ref triangleCount, ref maxArea, ref avgArea, ref minArea); factorArea = factorArea != 0 ? math.clamp(factorArea, kMinAreaFactor, kMaxAreaFactor) : factorArea; var criArea = maxArea * factorArea; criArea = math.max(criArea, targetArea); // Refine while (!refined && validGraph) { // Check if any of the Triangle is Invalid or Segment is invalid. If yes, Refine. for (int i = 0; i < triangleCount; ++i) { if (RequiresRefining(triangles[i], criArea)) { invalidTriangle = i; break; } } // Find any Segment that can be Split based on the Input Length. // todo. if (invalidTriangle != -1) { // Get all Segments that are encroached. var t = triangles[invalidTriangle]; var encroachCount = 0; FetchEncroachedSegments(pgPoints, pgPointCount, pgEdges, pgEdgeCount, ref encroach, ref encroachCount, t.c); // Split each Encroached Segments. If no segments are encroached. Split the Triangle. if (encroachCount != 0) { for (int i = 0; i < encroachCount; ++i) { SplitSegments(ref pgPoints, ref pgPointCount, ref pgEdges, ref pgEdgeCount, encroach[i]); } } else { // Update Triangulation. var split = t.c.center; pgPoints[pgPointCount++] = split; } // Tessellate again. indexCount = 0; vertexCount = 0; validGraph = Tessellator.Tessellate(allocator, pgPoints, pgPointCount, pgEdges, pgEdgeCount, ref vertices, ref vertexCount, ref indices, ref indexCount); // Build Internal Triangles. encroachCount = 0; triangleCount = 0; invalidTriangle = -1; if (validGraph) { ModuleHandle.BuildTriangles(vertices, vertexCount, indices, indexCount, ref triangles, ref triangleCount, ref maxArea, ref avgArea, ref minArea); } // More than enough Steiner points inserted. This handles all sort of weird input sprites very well (even random point cloud). if (pgPointCount - inputPointCount > kMaxSteinerCount) { break; } } else { refined = true; } } // Dispose off triangles.Dispose(); encroach.Dispose(); return(refined); }
// Perform Voronoi based Smoothing. Does not add/remove points but merely relocates internal vertices so they are uniform distributed. internal static bool Condition(Allocator allocator, ref NativeArray <float2> pgPoints, int pgPointCount, NativeArray <int2> pgEdges, int pgEdgeCount, ref NativeArray <float2> vertices, ref int vertexCount, ref NativeArray <int> indices, ref int indexCount) { // Build Triangles and Edges. float maxArea = 0, cmxArea = 0, minArea = 0, cmnArea = 0, avgArea = 0, minEdge = 0, maxEdge = 0, avgEdge = 0; bool polygonCentroid = true, validGraph = true; int triangleCount = 0, delaEdgeCount = 0, affectingEdgeCount = 0; var triangles = new NativeArray <UTriangle>(indexCount, allocator); // Intentionally added more room than actual Triangles needed here. var delaEdges = new NativeArray <int4>(indexCount, allocator); var voronoiEdges = new NativeArray <int4>(indexCount, allocator); var connectedTri = new NativeArray <int4>(vertexCount, allocator); var voronoiCheck = new NativeArray <int>(indexCount, allocator); var affectsEdges = new NativeArray <int>(indexCount, allocator); var triCentroids = new NativeArray <int>(vertexCount, allocator); ModuleHandle.BuildTrianglesAndEdges(vertices, vertexCount, indices, indexCount, ref triangles, ref triangleCount, ref delaEdges, ref delaEdgeCount, ref maxArea, ref avgArea, ref minArea); var refinedEdges = new NativeArray <int4>(delaEdgeCount, allocator); // Sort the Delaunay Edges. unsafe { ModuleHandle.InsertionSort <int4, DelaEdgeCompare>( NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(delaEdges), 0, delaEdgeCount - 1, new DelaEdgeCompare()); } // TrimEdges. Update Triangle Info for Shared Edges and remove Duplicates. RefineEdges(ref refinedEdges, ref delaEdges, ref delaEdgeCount, ref voronoiEdges); // Now for each point, generate Voronoi diagram. for (int i = 0; i < vertexCount; ++i) { // Try moving this to Centroid of the Voronoi Polygon. GetAffectingEdges(i, delaEdges, delaEdgeCount, ref affectsEdges, ref voronoiCheck, ref affectingEdgeCount); var bounded = affectingEdgeCount != 0; // Check for Boundedness for (int j = 0; j < affectingEdgeCount; ++j) { // Edge Index. var ei = affectsEdges[j]; if (delaEdges[ei].z == -1 || delaEdges[ei].w == -1) { bounded = false; break; } } // If this is bounded point, relocate to Voronoi Diagram's Centroid if (bounded) { polygonCentroid = ConnectTriangles(ref connectedTri, ref affectsEdges, ref voronoiCheck, voronoiEdges, affectingEdgeCount); if (!polygonCentroid) { break; } float2 point = float2.zero; float area = 0, distance = 0; for (int k = 0; k < affectingEdgeCount; ++k) { CentroidByPolygon(connectedTri[k], triangles, ref point, ref area, ref distance); } point /= (3 * area); pgPoints[i] = point; } } // Do Delaunay Again. int srcIndexCount = indexCount, srcVertexCount = vertexCount; indexCount = 0; vertexCount = 0; triangleCount = 0; if (polygonCentroid) { validGraph = Tessellator.Tessellate(allocator, pgPoints, pgPointCount, pgEdges, pgEdgeCount, ref vertices, ref vertexCount, ref indices, ref indexCount); if (validGraph) { ModuleHandle.BuildTriangles(vertices, vertexCount, indices, indexCount, ref triangles, ref triangleCount, ref cmxArea, ref avgArea, ref cmnArea, ref maxEdge, ref avgEdge, ref minEdge); } // This Edge validation prevents artifacts by forcing a fallback. todo: Fix the actual bug in Outline generation. validGraph = validGraph && (cmxArea < maxArea * kMaxAreaTolerance) && (maxEdge < avgEdge * kMaxEdgeTolerance); } // Cleanup. triangles.Dispose(); delaEdges.Dispose(); refinedEdges.Dispose(); voronoiCheck.Dispose(); voronoiEdges.Dispose(); affectsEdges.Dispose(); triCentroids.Dispose(); connectedTri.Dispose(); return(validGraph && srcIndexCount == indexCount && srcVertexCount == vertexCount); }
internal bool Triangulate(NativeArray <float2> points, int pointCount, NativeArray <int2> edges, int edgeCount) { m_NumEdges = edgeCount; m_NumHulls = edgeCount * 2; m_NumPoints = pointCount; m_CellCount = 0; m_Cells = new NativeArray <int3>(ModuleHandle.kMaxTriangleCount, m_Allocator); m_ILArray = new NativeArray <int>(m_NumHulls * (m_NumHulls + 1), m_Allocator); // Make room for -1 node. m_IUArray = new NativeArray <int>(m_NumHulls * (m_NumHulls + 1), m_Allocator); // Make room for -1 node. NativeArray <UHull> hulls = new NativeArray <UHull>(m_NumPoints * 8, m_Allocator); int hullCount = 0; NativeArray <UEvent> events = new NativeArray <UEvent>(m_NumPoints + (m_NumEdges * 2), m_Allocator); int eventCount = 0; for (int i = 0; i < m_NumPoints; ++i) { UEvent evt = new UEvent(); evt.a = points[i]; evt.b = new float2(); evt.idx = i; evt.type = (int)UEventType.EVENT_POINT; events[eventCount++] = evt; } for (int i = 0; i < m_NumEdges; ++i) { int2 e = edges[i]; float2 a = points[e.x]; float2 b = points[e.y]; if (a.x < b.x) { UEvent _s = new UEvent(); _s.a = a; _s.b = b; _s.idx = i; _s.type = (int)UEventType.EVENT_START; UEvent _e = new UEvent(); _e.a = b; _e.b = a; _e.idx = i; _e.type = (int)UEventType.EVENT_END; events[eventCount++] = _s; events[eventCount++] = _e; } else if (a.x > b.x) { UEvent _s = new UEvent(); _s.a = b; _s.b = a; _s.idx = i; _s.type = (int)UEventType.EVENT_START; UEvent _e = new UEvent(); _e.a = a; _e.b = b; _e.idx = i; _e.type = (int)UEventType.EVENT_END; events[eventCount++] = _s; events[eventCount++] = _e; } } unsafe { ModuleHandle.InsertionSort <UEvent, TessEventCompare>( NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(events), 0, eventCount - 1, new TessEventCompare()); ; } var hullOp = true; float minX = events[0].a.x - (1 + math.abs(events[0].a.x)) * math.pow(2.0f, -16.0f); UHull hull; hull.a.x = minX; hull.a.y = 1; hull.b.x = minX; hull.b.y = 0; hull.idx = -1; hull.ilarray = new ArraySlice <int>(m_ILArray, m_NumHulls * m_NumHulls, m_NumHulls); // Last element hull.iuarray = new ArraySlice <int>(m_IUArray, m_NumHulls * m_NumHulls, m_NumHulls); hull.ilcount = 0; hull.iucount = 0; hulls[hullCount++] = hull; for (int i = 0, numEvents = eventCount; i < numEvents; ++i) { switch (events[i].type) { case (int)UEventType.EVENT_POINT: { hullOp = AddPoint(hulls, hullCount, points, events[i].a, events[i].idx); } break; case (int)UEventType.EVENT_START: { hullOp = SplitHulls(hulls, ref hullCount, points, events[i]); } break; default: { hullOp = MergeHulls(hulls, ref hullCount, points, events[i]); } break; } if (!hullOp) { break; } } events.Dispose(); hulls.Dispose(); return(hullOp); }
NativeArray <int3> Constrain(ref int count) { var cells = GetCells(ref count); int nc = count; for (int i = 0; i < nc; ++i) { int3 c = cells[i]; int x = c.x, y = c.y, z = c.z; if (y < z) { if (y < x) { c.x = y; c.y = z; c.z = x; } } else if (z < x) { c.x = z; c.y = x; c.z = y; } cells[i] = c; } unsafe { ModuleHandle.InsertionSort <int3, TessCellCompare>( NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(cells), 0, m_CellCount - 1, new TessCellCompare()); } // Out m_Flags = new NativeArray <int>(nc, m_Allocator); m_Neighbors = new NativeArray <int>(nc * 3, m_Allocator); m_Constraints = new NativeArray <int>(nc * 3, m_Allocator); var next = new NativeArray <int>(nc * 3, m_Allocator); var active = new NativeArray <int>(nc * 3, m_Allocator); int side = 1, nextCount = 0, activeCount = 0; for (int i = 0; i < nc; ++i) { int3 c = cells[i]; for (int j = 0; j < 3; ++j) { int x = j, y = (j + 1) % 3; x = (x == 0) ? c.x : (j == 1) ? c.y : c.z; y = (y == 0) ? c.x : (y == 1) ? c.y : c.z; int o = OppositeOf(y, x); int a = m_Neighbors[3 * i + j] = FindNeighbor(cells, count, y, x, o); int b = m_Constraints[3 * i + j] = (-1 != FindConstraint(x, y)) ? 1 : 0; if (a < 0) { if (0 != b) { next[nextCount++] = i; } else { active[activeCount++] = i; m_Flags[i] = 1; } } } } while (activeCount > 0 || nextCount > 0) { while (activeCount > 0) { int t = active[activeCount - 1]; activeCount--; if (m_Flags[t] == -side) { continue; } m_Flags[t] = side; int3 c = cells[t]; for (int j = 0; j < 3; ++j) { int f = m_Neighbors[3 * t + j]; if (f >= 0 && m_Flags[f] == 0) { if (0 != m_Constraints[3 * t + j]) { next[nextCount++] = f; } else { active[activeCount++] = f; m_Flags[f] = side; } } } } for (int e = 0; e < nextCount; e++) { active[e] = next[e]; } activeCount = nextCount; nextCount = 0; side = -side; } active.Dispose(); next.Dispose(); return(cells); }
internal bool ApplyDelaunay(NativeArray <float2> points, NativeArray <int2> edges) { NativeArray <int> stack = new NativeArray <int>(m_NumPoints * (m_NumPoints + 1), m_Allocator); int stackCount = 0; var valid = true; PrepareDelaunay(edges, m_NumEdges); for (int a = 0; valid && (a < m_NumPoints); ++a) { UStar star = m_Stars[a]; for (int j = 1; j < star.pointCount; j += 2) { int b = star.points[j]; if (b < a) { continue; } if (FindConstraint(a, b) >= 0) { continue; } int x = star.points[j - 1], y = -1; for (int k = 1; k < star.pointCount; k += 2) { if (star.points[k - 1] == b) { y = star.points[k]; break; } } if (y < 0) { continue; } if (ModuleHandle.IsInsideCircle(points[a], points[b], points[x], points[y])) { if ((2 + stackCount) >= stack.Length) { valid = false; break; } stack[stackCount++] = a; stack[stackCount++] = b; } } } var flipFlops = m_NumPoints * m_NumPoints; while (stackCount > 0 && valid) { int b = stack[stackCount - 1]; stackCount--; int a = stack[stackCount - 1]; stackCount--; int x = -1, y = -1; UStar star = m_Stars[a]; for (int i = 1; i < star.pointCount; i += 2) { int s = star.points[i - 1]; int t = star.points[i]; if (s == b) { y = t; } else if (t == b) { x = s; } } if (x < 0 || y < 0) { continue; } if (!ModuleHandle.IsInsideCircle(points[a], points[b], points[x], points[y])) { continue; } EdgeFlip(a, b); valid = Flip(points, ref stack, ref stackCount, x, a, y); valid = valid && Flip(points, ref stack, ref stackCount, a, y, x); valid = valid && Flip(points, ref stack, ref stackCount, y, b, x); valid = valid && Flip(points, ref stack, ref stackCount, b, x, y); valid = valid && (--flipFlops > 0); } stack.Dispose(); return(valid); }
public bool Test(UHull h, float2 p, ref float t) { t = ModuleHandle.OrientFast(h.a, h.b, p); return(t > 0); }
void PrepareDelaunay(NativeArray <int2> edges, int edgeCount) { m_StarCount = m_CellCount * 3; m_Stars = new NativeArray <UStar>(m_StarCount, m_Allocator); m_SPArray = new NativeArray <int>(m_StarCount * m_StarCount, m_Allocator); var UEdgeCount = 0; var UEdges = new NativeArray <int2>(m_StarCount, m_Allocator); // Input Edges. for (int i = 0; i < edgeCount; ++i) { int2 e = edges[i]; e.x = (edges[i].x < edges[i].y) ? edges[i].x : edges[i].y; e.y = (edges[i].x > edges[i].y) ? edges[i].x : edges[i].y; edges[i] = e; InsertUniqueEdge(UEdges, e, ref UEdgeCount); } m_Edges = new NativeArray <int2>(UEdgeCount, m_Allocator); for (int i = 0; i < UEdgeCount; ++i) { m_Edges[i] = UEdges[i]; } UEdges.Dispose(); unsafe { ModuleHandle.InsertionSort <int2, TessEdgeCompare>( NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(m_Edges), 0, m_Edges.Length - 1, new TessEdgeCompare()); } // Init Stars. for (int i = 0; i < m_StarCount; ++i) { UStar s = m_Stars[i]; s.points = new ArraySlice <int>(m_SPArray, i * m_StarCount, m_StarCount); s.pointCount = 0; m_Stars[i] = s; } // Fill stars. for (int i = 0; i < m_CellCount; ++i) { int a = m_Cells[i].x; int b = m_Cells[i].y; int c = m_Cells[i].z; UStar sa = m_Stars[a]; UStar sb = m_Stars[b]; UStar sc = m_Stars[c]; sa.points[sa.pointCount++] = b; sa.points[sa.pointCount++] = c; sb.points[sb.pointCount++] = c; sb.points[sb.pointCount++] = a; sc.points[sc.pointCount++] = a; sc.points[sc.pointCount++] = b; m_Stars[a] = sa; m_Stars[b] = sb; m_Stars[c] = sc; } }