module; #include export module compile.mapmesh; import utils; import io; import AssetData; import std; struct Pt; namespace ee_sdk { #pragma pack(push, 1) struct BMPFileHeader { std::uint16_t bfType; std::uint32_t bfSize; std::uint16_t bfReserved1; std::uint16_t bfReserved2; std::uint32_t bfOffBits; }; struct BMPInfoHeader { std::uint32_t biSize; std::int32_t biWidth; std::int32_t biHeight; std::uint16_t biPlanes; std::uint16_t biBitCount; std::uint32_t biCompression; std::uint32_t biSizeImage; std::int32_t biXPelsPerMeter; std::int32_t biYPelsPerMeter; std::uint32_t biClrUsed; std::uint32_t biClrImportant; }; #pragma pack(pop) using BGR = glm::vec<3, std::uint8_t>; struct BGRHash { std::size_t operator()(const BGR& c) const noexcept {return colorToHex(c.r, c.g, c.b, true);} }; struct prov { std::vector vertices; // pixels indices std::vector indices; }; using PixelMesh = std::unordered_map,BGRHash>; struct Image { std::int32_t width; std::int32_t height; std::size_t rowSize; std::vector image; std::vector tempImage; }; class Point { public: std::int32_t x, y; std::size_t id; Point() = default; Point (const std::size_t i, const std::size_t width) { x = i % width; y = i / width; id = i; } static std::int64_t Cross(const Point &a, const Point &b, const Point &c) { return 1LL * (b.x - a.x) * (c.y - a.y) - 1LL * (b.y - a.y) * (c.x - a.x); } static std::int64_t SignedArea2(const std::span &p) { std::int64_t a = 0; for (std::size_t i = 0; i < p.size(); i++) { const Point &p0 = p[i]; const Point& p1 = p[(i + 1) % p.size()]; a += 1LL * p0.x * p1.y - 1LL * p1.x * p0.y; } return a; } bool IsInTriangle(const Point &a, const Point &b, const Point &c) const { const std::int64_t c1 = Cross(a, b, *this); const std::int64_t c2 = Cross(b, c, *this); const std::int64_t c3 = Cross(c, a, *this); return (c1 >= 0 && c2 >= 0 && c3 >= 0) || (c1 <= 0 && c2 <= 0 && c3 <= 0); } }; } export namespace ee_sdk { class MapMeshCompiler { public: static ee_engine::MapMesh CompileBMP(std::span asset) { ee_engine::MapMesh mesh; // Read BMP file info from header BMPFileHeader fileHeader; BMPInfoHeader infoHeader; std::memcpy(&fileHeader, asset.data(), sizeof(BMPFileHeader)); std::memcpy(&infoHeader, asset.data() + sizeof(BMPFileHeader), sizeof(BMPInfoHeader)); Image imageInfo { .width = infoHeader.biWidth, .height = infoHeader.biHeight, .rowSize = static_cast(infoHeader.biWidth)*3+3&~3 }; // Copy file to buffers for algorithms imageInfo.image.resize(imageInfo.rowSize * imageInfo.height); imageInfo.tempImage.resize(imageInfo.rowSize * imageInfo.height); std::memcpy(imageInfo.image.data(), asset.data() + fileHeader.bfOffBits, imageInfo.image.size()); std::memcpy(imageInfo.tempImage.data(), asset.data() + fileHeader.bfOffBits, imageInfo.tempImage.size()); PixelMesh pixelMesh; // Extract mesh from image FloodFill(imageInfo, pixelMesh); FindContour(imageInfo, pixelMesh); Aproximate(imageInfo, pixelMesh, 1); Triangulate(imageInfo, pixelMesh); Optimize(pixelMesh); // Provide mesh in suitable format for(auto&[color, provs]: pixelMesh) { std::vector outProvinces; for(auto&[vertices, indices]: provs) { ee_engine::MapMesh::Province outProvince; //Skip invalid meshes if (indices.size() % 3 != 0) { vertices.clear(); indices.clear(); } for(std::size_t i: vertices) { glm::vec2 v{ static_cast(i % imageInfo.width), static_cast(i / imageInfo.width) }; outProvince.vertices.push_back(v); } outProvince.indices = indices; outProvinces.push_back(outProvince); } mesh.regions[colorToHex(color.r, color.g, color.b, true)] = outProvinces; } LOG(INFO, "COMPILED MAPMESH: generated [%d] regions", mesh.regions.size()); return mesh; } private: static constexpr BGR EMPTY_COLOR = {255, 255, 255}; static inline BGR getPixelSafe(const Image &image, const std::size_t r, const std::size_t c) { if(r >= image.height || c >= image.width) return EMPTY_COLOR; return *reinterpret_cast( image.image.data() + r * image.rowSize + c * 3 ); } static inline BGR getPixelSafeIndex(const Image &image, const std::size_t i) { return getPixelSafe(image, i / image.width, i % image.width); } static void FloodFill(Image &image, PixelMesh &mesh) { const std::size_t size = image.width * image.height; for(std::size_t i = 0; i < size; i++) { BGR color= *reinterpret_cast(image.tempImage.data() + (i/image.width)*image.rowSize + (i%image.width)*3); if(color == EMPTY_COLOR) continue; std::vector prov; std::queue q; q.push(i); while (!q.empty()) { std::size_t n = q.front(); q.pop(); BGR& px = *reinterpret_cast(image.tempImage.data() + (n/image.width)*image.rowSize + (n%image.width)*3); if(px != color) continue; px = EMPTY_COLOR; prov.push_back(n); if(n >= image.width) q.push(n - image.width); if(n + image.width < size) q.push(n + image.width); if(n % image.width) q.push(n - 1); if(n % image.width + 1 < image.width) q.push(n + 1); } mesh[color].push_back({.vertices = prov}); } } static bool MooreStep( const Image &image, const std::size_t cur, const std::size_t prev, const BGR id, std::size_t &next ) { struct offset{int8_t x,y;}; static const offset moore[8]={ {1,0},{1,-1},{0,-1},{-1,-1}, {-1,0},{-1,1},{0,1},{1,1} }; const std::int32_t cr = cur / image.width, cc = cur % image.width; const std::int32_t pr = prev / image.width, pc = prev % image.width; const std::int32_t dx = cc - pc, dy = cr - pr; std::int32_t startDir = 0; for(std::int32_t i = 0; i < 8; i++) { if(moore[i].x == dx && moore[i].y == dy) startDir=(i+6)&7; } for(std::int32_t k=0; k<8; k++){ const std::int32_t d = (startDir+k)&7; const std::int32_t nr = cr + moore[d].y; if( const std::int32_t nc = cc + moore[d].x; getPixelSafe(image, nr,nc) == id ){ next= static_cast(nr) * image.width + static_cast(nc); return true; } } return false; } static std::size_t FindStart(const Image &image, const std::vector&prov, const BGR &id) { for(const std::size_t i : prov){ const std::size_t r = i / image.width; const std::size_t c = i % image.width; if(getPixelSafe(image, r - 1, c) != id|| getPixelSafe(image, r + 1, c) != id|| getPixelSafe(image, r, c- 1) != id|| getPixelSafe(image, r, c+ 1) != id) return i; } return std::numeric_limits::max(); } static void FindContour(const Image &image, PixelMesh &mesh) { for(auto &provs : mesh | std::views::values){ for(auto&[vertices, indices] : provs){ if(vertices.empty()) continue; BGR id = getPixelSafeIndex(image, vertices[0]); std::size_t start = FindStart(image, vertices,id); if(start == std::numeric_limits::max()) continue; std::vector contour; contour.push_back(start); std::size_t cur=start; std::size_t prev = (start % image.width > 0) ? start - 1 : start + 1; const std::size_t MAX = vertices.size()*8; for(std::size_t i = 0 ;i < MAX; i++){ std::size_t next; if(!MooreStep(image, cur, prev, id, next)) break; if(next == start) break; contour.push_back(next); prev = cur; cur = next; } vertices.swap(contour); } } } static double PerpendicularDistance( const std::size_t p, const std::size_t a, const std::size_t b, const std::size_t width ) { const double px = p % width; const double py = p / width; const double ax = a % width; const double ay = a / width; const double bx = b % width; const double by = b / width; const double dx = bx - ax; const double dy = by - ay; if(dx == 0 && dy == 0) return std::hypot(px - ax, py - ay); const double num = std::abs(dy * px - dx * py + bx * ay - by * ax); const double den = std::sqrt(dx * dx + dy * dy); return num / den; } // Douglas-Peucker iterative static void Aproximate(const Image &image, PixelMesh &mesh, const std::size_t epsilon) { for(auto &provs : mesh | std::views::values) { for(auto&[vertices, indices] : provs) { if(vertices.size() < 3) continue; const std::size_t n = vertices.size(); std::vector keep_point(n, true); std::stack> st; // в стек кладем первый и последний индекс st.push({0, n - 1}); while(!st.empty()) { auto [startIndex, endIndex] = st.top(); st.pop(); double dMax = 0.0; std::size_t index = startIndex; for(std::size_t i = startIndex + 1; i < endIndex; ++i) { if(keep_point[i]) { double d = PerpendicularDistance( vertices[i], vertices[startIndex], vertices[endIndex], image.width ); if(d > dMax) { dMax = d; index = i; } } } if(dMax >= epsilon) { st.push({startIndex, index}); st.push({index, endIndex}); } else { for(size_t j = startIndex + 1; j < endIndex; ++j) keep_point[j] = false; } } // формируем итоговый список std::vector result; result.reserve(n); for(std::size_t i = 0; i < n; ++i) { if(keep_point[i]) result.push_back(vertices[i]); } vertices.swap(result); } } } static std::vector TriangulateProv(const std::vector &prov, const std::size_t width) { std::vector poly; poly.reserve(prov.size()); for (const std::size_t i : prov) poly.push_back(Point(i, width)); // CCW if (Point::SignedArea2(poly) < 0) std::ranges::reverse(poly); std::vector out; // a,b,c,a,b,c,... while (poly.size() > 3) { bool ear_found = false; for (std::size_t i = 0; i < poly.size(); ++i) { const std::size_t i0 = (i + poly.size() - 1) % poly.size(); const std::size_t i1 = i; const std::size_t i2 = (i + 1) % poly.size(); const Point& a = poly[i0]; const Point& b = poly[i1]; const Point& c = poly[i2]; if (Point::Cross(a, b, c) <= 0) continue; // не выпуклая bool inside = false; for (std::size_t j = 0; j < poly.size(); ++j) { if (j == i0 || j == i1 || j == i2) continue; if (poly[j].IsInTriangle(a, b, c)) { inside = true; break; } } if (!inside) { out.push_back(a.id); out.push_back(b.id); out.push_back(c.id); poly.erase(poly.begin() + i1); ear_found = true; break; } } if (!ear_found) { // плохой контур (самопересечение / коллинеарность) break; } } if (poly.size() == 3) { out.push_back(poly[0].id); out.push_back(poly[1].id); out.push_back(poly[2].id); } return out; } static void Triangulate(const Image &image, PixelMesh &pixelMesh) { for(auto &provs : pixelMesh | std::views::values) { for(auto&[vertices, indices] : provs) { if (vertices.size() < 3) continue; vertices = TriangulateProv(vertices, image.width); } } } static void Optimize(PixelMesh &pixelMesh) { for(auto &provs : pixelMesh | std::views::values) { for(auto&[vertices, indices] : provs) { std::unordered_map vertexToIndex; vertexToIndex.reserve(vertices.size()); std::vector uniqueVertices; uniqueVertices.reserve(vertices.size()); indices.clear(); for (std::size_t v : vertices) { if (auto it = vertexToIndex.find(v); it != vertexToIndex.end()) { indices.push_back(it->second); } else { std::uint16_t newIndex = static_cast(uniqueVertices.size()); uniqueVertices.push_back(v); vertexToIndex[v] = newIndex; indices.push_back(newIndex); } } vertices.swap(uniqueVertices); } } } }; }