void Tessel::PolygonVertex(Vector V) { int index = Mesh::AddVertex(V); PolygonVertex(index); }
bool PhysicsInterface::convertImageAlphaTo2DPolygons(const Image& image, Vector<Vector<Vec2>>& outPolygons, bool flipHorizontally, bool flipVertically) { // Check image is valid if (!image.isValid2DImage()) { LOG_ERROR << "The passed image is not a valid 2D image: " << image; return false; } auto bitmap = Bitmap(image.getWidth(), image.getHeight()); // Setup bitmap contents auto width = int(image.getWidth()); for (auto i = 0U; i < bitmap.data.size(); i++) bitmap.set(i % width, i / width, image.getPixelColor(i % width, i / width).a > 0.5f); // Find all the edge pixels auto edgePixels = Vector<PolygonVertex>(); for (auto y = 0; y < bitmap.height; y++) { for (auto x = 0; x < bitmap.width; x++) { if (bitmap.get(x, y) && (!bitmap.get(x - 1, y - 1) || !bitmap.get(x - 1, y) || !bitmap.get(x - 1, y + 1) || !bitmap.get(x, y - 1) || !bitmap.get(x, y + 1) || !bitmap.get(x + 1, y - 1) || !bitmap.get(x + 1, y) || !bitmap.get(x + 1, y + 1))) edgePixels.emplace(x, y); } } while (!edgePixels.empty()) { // Start the next polygon at an unused edge pixel auto polygon = Vector<PolygonVertex>(1, edgePixels.popBack()); // Each pixel that is put onto polygon can be backtracked if it leads to a dead end, this fixes problems with // pointy angles that can cause the edge walking to get stuck. auto hasBacktracked = false; while (true) { // Continue building this polygon by finding the next adjacent edge pixel auto adjacentPixel = 0U; for (; adjacentPixel < edgePixels.size(); adjacentPixel++) { if (bitmap.isAdjacent(polygon.back(), edgePixels[adjacentPixel])) break; } // If there was no adjacent edge pixel then this polygon is malformed, so skip it and keep trying to build // more if (adjacentPixel == edgePixels.size()) { if (!hasBacktracked) { polygon.popBack(); hasBacktracked = true; if (polygon.empty()) break; continue; } else break; } // Add the adjacent edge pixel to this polygon polygon.append(edgePixels[adjacentPixel]); edgePixels.erase(adjacentPixel); hasBacktracked = false; // Check whether this polygon is now complete, at least 4 points are required for a valid polygon if (polygon.size() < 4 || !bitmap.isAdjacent(polygon[0], polygon.back())) continue; // Now that a complete polygon has been constructed it needs to be simplified down as much as possible while // retaining key features such as large straight edges and right angles // Simplify perfectly horizontal and vertical edges as much as possible for (auto i = 0; i < int(polygon.size()); i++) { const auto& a = polygon[i]; const auto& b = polygon[(i + 1) % polygon.size()]; const auto& c = polygon[(i + 2) % polygon.size()]; if ((a.x == b.x && a.x == c.x) || (a.y == b.y && a.y == c.y)) polygon.erase((i-- + 1) % polygon.size()); } // Identify horizontal and vertical edges that are on the outside edge of the bitmap and mark their vertices // as important for (auto i = 0U; i < polygon.size(); i++) { auto& a = polygon[i]; auto& b = polygon[(i + 1) % polygon.size()]; if ((a.x == 0 || a.x == int(image.getWidth() - 1) || a.y == 0 || a.y == int(image.getHeight() - 1)) && a.isAxialEdge(b)) { a.keep = true; b.keep = true; } } // Identify axial right angles and flag the relevant vertices as important for (auto i = 0U; i < polygon.size(); i++) { const auto& a = polygon[i]; const auto& c = polygon[(i + 2) % polygon.size()]; auto& b = polygon[(i + 1) % polygon.size()]; if (a.isAxialEdge(b) && b.isAxialEdge(c) && (a - b).isRightAngle(c - b)) b.keep = true; } // The ends of straight edges that are not part of a right angle shape are pulled inwards by inserting new // vertices one pixel apart, this allows the ends of straight edges to undergo subsequent simplification. // The 'body' of the straight edge is then flagged as important to avoid any further simplification, which // will preserve the straight edge in the final result. const auto straightEdgePullBackSize = straightEdgeLength / 3; for (auto i = 0U; i < polygon.size(); i++) { auto a = polygon[i]; auto b = polygon[(i + 1) % polygon.size()]; if (a.isAxialEdge(b)) { auto xSign = Math::getSign(b.x - a.x); auto ySign = Math::getSign(b.y - a.y); if (!a.keep) { for (auto j = 0U; j < straightEdgePullBackSize; j++) polygon.insert(i++, PolygonVertex(a.x + xSign * (j + 1), a.y + (j + 1) * ySign)); polygon[i].keep = true; } if (!b.keep) { for (auto j = 0U; j < straightEdgePullBackSize; j++) { polygon.insert(i++, PolygonVertex(b.x - (straightEdgePullBackSize - j) * xSign, b.y - (straightEdgePullBackSize - j) * ySign)); } polygon[i - straightEdgePullBackSize + 1].keep = true; } } } // This is the main simplification loop, it works by trying to do progressively larger and larger // simplifcations on the polygon auto simplificationThreshold = 1.5f; while (polygon.size() > 3) { for (auto i = 0U; i < polygon.size(); i++) { const auto& a = polygon[i]; const auto& b = polygon[(i + 1) % polygon.size()]; const auto& c = polygon[(i + 2) % polygon.size()]; // If b is important then don't try to get rid of it if (b.keep) continue; // Get rid of point b if the line a-c is connected by an edge in the bitmap if (a.distance(c) < simplificationThreshold && bitmap.arePixelsConnectedByEdge(a, c)) polygon.erase((i + 1) % polygon.size()); } simplificationThreshold += 1.0f; if (simplificationThreshold >= std::max(image.getWidth(), image.getHeight())) break; } if (polygon.size() < 3) break; outPolygons.enlarge(1); auto& outPolygon = outPolygons.back(); // Scale to the range 0-1 for (const auto& vertex : polygon) outPolygon.append(vertex.toVec2() / Vec2(float(image.getWidth() - 1), float(image.getHeight() - 1))); // Apply horizontal and vertical flips if requested if (flipHorizontally) { for (auto& vertex : outPolygon) vertex.setXY(1.0f - vertex.x, vertex.y); } if (flipVertically) { for (auto& vertex : outPolygon) vertex.setXY(vertex.x, 1.0f - vertex.y); } // Order vertices clockwise auto center = outPolygon.getAverage(); if ((Vec3(outPolygon[0]) - center).cross(Vec3(outPolygon[1]) - center).z > 0.0f) outPolygon.reverse(); break; } } return !outPolygons.empty(); }