diff --git a/eqglib/eqg_terrain.cpp b/eqglib/eqg_terrain.cpp index 448de117..d45ccf17 100644 --- a/eqglib/eqg_terrain.cpp +++ b/eqglib/eqg_terrain.cpp @@ -284,6 +284,7 @@ bool Terrain::InitFromWLDData(const STerrainWLDData& wldData) area.tag = wldArea.tag; area.userData = wldArea.userData; + area.areaNum = areaNum; area.regionNumbers.resize(wldArea.numRegions); memcpy(area.regionNumbers.data(), wldArea.regions, sizeof(uint32_t) * wldArea.numRegions); @@ -460,6 +461,7 @@ bool Terrain::InitFromWLDData(const STerrainWLDData& wldData) if (!m_wldAreas.empty()) { m_wldAreaEnvironments.resize(m_numWLDRegions); + m_wldAreaEnvironmentsPerArea.resize(m_wldAreas.size()); for (const SArea& area : m_wldAreas) { @@ -510,6 +512,7 @@ bool Terrain::InitFromWLDData(const STerrainWLDData& wldData) } } + m_wldAreaEnvironmentsPerArea[area.areaNum] = env; for (uint32_t regionNum : area.regionNumbers) { m_wldAreaEnvironments[regionNum] = env; diff --git a/eqglib/eqg_terrain.h b/eqglib/eqg_terrain.h index 041f74c1..7308eefd 100644 --- a/eqglib/eqg_terrain.h +++ b/eqglib/eqg_terrain.h @@ -85,6 +85,7 @@ struct SArea { std::string tag; std::string userData; + uint32_t areaNum; std::vector regionNumbers; std::vector centers; }; @@ -148,6 +149,7 @@ class Terrain std::vector m_wldAreas; std::vector m_wldAreaIndices; std::vector m_wldAreaEnvironments; + std::vector m_wldAreaEnvironmentsPerArea; std::shared_ptr m_wldBspTree; // EQG areas diff --git a/meshgen/AreaVolumeRenderSystem.cpp b/meshgen/AreaVolumeRenderSystem.cpp index 94579070..6633e664 100644 --- a/meshgen/AreaVolumeRenderSystem.cpp +++ b/meshgen/AreaVolumeRenderSystem.cpp @@ -177,7 +177,8 @@ void AreaVolumeRenderSystem::RebuildBuffers() { // Combine volumes in this color group std::vector vertices; - std::vector> faces; + std::vector> faces; + std::vector> edges; std::vector debugFaceColors; // Per-face colors for debug mode for (entt::entity entity : group) @@ -200,13 +201,19 @@ void AreaVolumeRenderSystem::RebuildBuffers() for (const auto& face : volumeComp.faces) { - std::vector adjustedFace; - adjustedFace.reserve(face.size()); - for (uint16_t idx : face) - { - adjustedFace.push_back(baseIndex + idx); - } - faces.push_back(std::move(adjustedFace)); + std::array adjustedFace{}; + adjustedFace[0] = face[0] + baseIndex; + adjustedFace[1] = face[1] + baseIndex; + adjustedFace[2] = face[2] + baseIndex; + faces.push_back(adjustedFace); + } + + for (const auto& edge : volumeComp.outerEdges) + { + std::array adjustedEdge{}; + adjustedEdge[0] = edge[0] + baseIndex; + adjustedEdge[1] = edge[1] + baseIndex; + edges.push_back(adjustedEdge); } } @@ -251,14 +258,10 @@ void AreaVolumeRenderSystem::RebuildBuffers() allVertices.push_back(v); } - // Fan triangulate this face - for (uint16_t i = 1; i + 1 < static_cast(face.size()); ++i) - { - // Front - allIndices.push_back(faceBaseVertex + 0); - allIndices.push_back(faceBaseVertex + i); - allIndices.push_back(faceBaseVertex + i + 1); - } + // Faces are triangulated + allIndices.push_back(faceBaseVertex + 0); + allIndices.push_back(faceBaseVertex + 1); + allIndices.push_back(faceBaseVertex + 2); } } else @@ -279,30 +282,27 @@ void AreaVolumeRenderSystem::RebuildBuffers() for (const auto& face : faces) { - for (size_t i = 0; i + 2 < face.size(); ++i) - { - // Front - allIndices.push_back(vertexOffset + face[0]); - allIndices.push_back(vertexOffset + face[i + 1]); - allIndices.push_back(vertexOffset + face[i + 2]); - // Back - //allIndices.push_back(vertexOffset + face[0]); - //allIndices.push_back(vertexOffset + face[i + 2]); - //allIndices.push_back(vertexOffset + face[i + 1]); - } + // Front + allIndices.push_back(vertexOffset + face[0]); + allIndices.push_back(vertexOffset + face[1]); + allIndices.push_back(vertexOffset + face[2]); + // Back + // allIndices.push_back(vertexOffset + face[0]); + // allIndices.push_back(vertexOffset + face[2]); + // allIndices.push_back(vertexOffset + face[1]); + } - for (size_t i = 0; i < face.size(); ++i) - { - uint16_t v0 = face[i]; - uint16_t v1 = face[(i + 1) % face.size()]; - if (v0 > v1) - std::swap(v0, v1); + for (const auto& edge : edges) + { + uint16_t a = edge[0]; + uint16_t b = edge[1]; + if (a > b) + std::swap(a, b); - const glm::vec3& vert0 = vertices[v0]; - const glm::vec3& vert1 = vertices[v1]; + const glm::vec3& vert0 = vertices[a]; + const glm::vec3& vert1 = vertices[b]; - allLineInstances.emplace_back(vert0, m_lineWidth, outlineCol, vert1, m_lineWidth, outlineCol); - } + allLineInstances.emplace_back(vert0, m_lineWidth, outlineCol, vert1, m_lineWidth, outlineCol); } } diff --git a/meshgen/EQComponents.h b/meshgen/EQComponents.h index 3b0ace6e..0a012ec1 100644 --- a/meshgen/EQComponents.h +++ b/meshgen/EQComponents.h @@ -73,7 +73,8 @@ struct WldAreaComponent struct AreaVolumeComponent { std::vector vertices; - std::vector> faces; // Polygon faces (not triangulated) + std::vector> faces; // explicitly triangulated faces + std::vector> outerEdges; }; // Render configuration for AreaVolumeComponent. diff --git a/meshgen/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index 64a9c09f..f67bc49c 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -7,21 +7,154 @@ #include "meshgen/EQComponents.h" #include "eqglib/eqg_terrain.h" +#include "CDT.h" +#include "clipper2/clipper.h" #include "glm/gtx/norm.hpp" #include "spdlog/spdlog.h" #include +#include #include #include #include #include + +constexpr float EPSILON = glm::epsilon(); + +// Orthonormal basis for projecting 3D points onto a plane +struct PlaneBasis +{ + glm::vec3 origin; // A point on the plane + glm::vec3 u; // First basis vector (tangent) + glm::vec3 v; // Second basis vector (bitangent) + + // Create basis from a plane + static PlaneBasis fromPlane(const Plane& plane) + { + PlaneBasis basis; + // Compute origin: closest point to world origin on the plane + basis.origin = plane.distance * plane.normal; + + // Compute orthonormal basis vectors on the plane + glm::vec3 up = glm::abs(plane.normal.y) < 0.9 ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0); + basis.u = glm::normalize(glm::cross(plane.normal, up)); + basis.v = glm::cross(plane.normal, basis.u); + return basis; + } + + // Project 3D point to 2D coordinates in this basis + [[nodiscard]] glm::vec2 project(const glm::vec3& point) const + { + glm::vec3 relative = point - origin; + return {glm::dot(relative, u), glm::dot(relative, v)}; + } + + // Unproject 2D coordinates back to 3D point on the plane + [[nodiscard]] glm::vec3 unproject(const glm::vec2& point) const + { + return origin + point.x * u + point.y * v; + } +}; + +// BSP node for area-specific trees +struct Node +{ + glm::vec3 normal; + float dist = 0.; + uint32_t region = 0; // Non-zero for leaf nodes (1-based region index) + uint32_t front = 0; // Front child index (0 = none, 1-based otherwise) + uint32_t back = 0; // Back child index (0 = none, 1-based otherwise) + + [[nodiscard]] Plane plane() const { return {normal, dist}; } +}; + +// Result structure for a single area's BSP tree +struct AreaBSPTree +{ + eqg::AreaEnvironment::Type type; + eqg::AreaEnvironment::Flags flags; + uint32_t areaNum; + uint32_t rootNum; + std::unordered_map nodes; +}; + +struct Vertex +{ + glm::vec3 position; + int id = -1; + + Vertex() = default; + explicit Vertex(const glm::vec3& position, int id) + : position(position), id(id) {} +}; + +struct Edge +{ + int start = -1; + int end = -1; + + Edge() = default; + explicit Edge(int start, int end) + : start(start), end(end) {} +}; + +struct Face +{ + std::vector vertexIds; + Plane plane; + int id = -1; + + Face() = default; + explicit Face(const Plane& plane, int id) + : plane(plane), id(id) {} +}; + +struct BRep +{ + + std::vector vertexes; + std::vector faces; + std::vector outerEdges; + + int addVertex(const glm::vec3& position) + { + const int id = static_cast(vertexes.size()); + vertexes.emplace_back(position, id); + return id; + } + + int addFace(const Plane& plane) + { + const int id = static_cast(faces.size()); + faces.emplace_back(plane, id); + return id; + } +}; + +struct MergeFace +{ + std::vector vertexes; // CCW order when viewed from normal direction + Plane plane; + + MergeFace() = default; + MergeFace(std::vector verts, const Plane& p) + : vertexes(std::move(verts)), plane(p) {} +}; + +struct Segment +{ + glm::vec3 start; + glm::vec3 end; +}; + + +#pragma region Polygon Clipping + //============================================================================================================ // Polygon Clipping for wld terrain regions (BSP Tree -> Convex Hull) //============================================================================================================ -#pragma region Polygon Clipping - // This is pretty intensive stuff, keep it optimized, even in debug #pragma optimize("t", on) #pragma warning(push) @@ -235,6 +368,507 @@ std::vector BuildConvexHullsFromRegions(const eqg::Terrain& te //============================================================================================================ //============================================================================================================ +#pragma region BSP Debugging Functions + +//============================================================================================================ +// Writes out BSP trees and OBJ files for debugging +//============================================================================================================ + +// Write a single node and recurse (pre-order: node, front, back) +static void writeNode(std::ostream& out, const Node& node, const AreaBSPTree& tree) +{ + if (node.region != 0) + { + out << "IN " << tree.areaNum << "\n"; + return; + } + + // Internal node + out << "PLANE " + << std::setprecision(9) << node.normal.x << " " + << std::setprecision(9) << node.normal.y << " " + << std::setprecision(9) << node.normal.z << " " + << std::setprecision(9) << node.dist << " " + << tree.areaNum << "\n"; + + if (node.front != 0 && tree.nodes.find(node.front) != tree.nodes.end()) + writeNode(out, tree.nodes.at(node.front), tree); + else + out << "NULL\n"; + + if (node.back != 0 && tree.nodes.find(node.back) != tree.nodes.end()) + writeNode(out, tree.nodes.at(node.back), tree); + else + out << "NULL\n"; +} + +bool saveBSP(const AreaBSPTree& tree, const std::string& filename) +{ + std::ofstream out(filename); + if (!out) + return false; + + out << "# BSP tree file\n"; + out << "BSP 1\n"; + + if (tree.nodes.find(tree.rootNum + 1) == tree.nodes.end()) + { + out << "NULL\n"; + return true; + } + + writeNode(out, tree.nodes.at(tree.rootNum + 1), tree); + return out.good(); +} + +bool saveOBJ(const BRepResult& brep, const std::string& filename) +{ + std::ofstream out(filename); + if (!out) + return false; + + out << std::fixed << std::setprecision(6); + + out << "# BRep exported to OBJ\n" + << "# Vertexes: " << brep.vertexes.size() << "\n" + << "# Faces: " << brep.faces.size() << "\n\n"; + + for (const auto& v : brep.vertexes) + out << "v " << v.x << " " << v.y << " " << v.z << "\n"; + + out << "\n"; + + for (const auto& verts : brep.faces) + { + if (verts.size() >= 3) + { + out << "f"; + for (const int v : verts) + out << " " << (v + 1); // OBJ uses 1-based indexing + out << "\n"; + } + } + + return out.good(); +} + +// Extracts BSP trees for individual areas from the full zone BSP tree. +// Each area gets its own subtree containing only the nodes that can reach +// regions belonging to that area. +std::vector BuildAreaBSPTrees(const eqg::Terrain& terrain) +{ + std::vector areaTrees; + + if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) + return areaTrees; + + const auto& fullTree = terrain.m_wldBspTree->nodes; + const auto& areas = terrain.m_wldAreas; + + if (areas.empty()) + return areaTrees; + + std::set unusedRegions; + for (uint32_t areaNum = 0; areaNum < areas.size(); ++areaNum) + { + if (areaNum < terrain.m_wldAreaIndices.size()) + for (uint32_t regionNum : areas[areaNum].regionNumbers) + unusedRegions.insert(regionNum); + } + + // For each area of contiguous environment type, extract a subtree + std::vector areaBSPTrees; + struct ExtractInfo + { + AreaBSPTree tree; + eqg::AreaEnvironment::Type envType = eqg::AreaEnvironment::Type_None; + eqg::AreaEnvironment::Flags envFlags = eqg::AreaEnvironment::Flags_None; + + explicit operator bool() const + { + return envType != eqg::AreaEnvironment::Type_None || envFlags != eqg::AreaEnvironment::Flags_None; + } + + bool operator!() const + { + return envType == eqg::AreaEnvironment::Type_None && envFlags == eqg::AreaEnvironment::Flags_None; + } + }; + + // Recursive function to determine if a subtree contains any regions + // belonging to this area, and if so, copy the relevant nodes. + std::function extractSubtree = [&](uint32_t nodeNum, ExtractInfo info) -> ExtractInfo + { + if (nodeNum > 0 && nodeNum <= fullTree.size()) + { + const auto& [plane, region, front, back] = fullTree[nodeNum - 1]; + + // check if this is a leaf node (region != 0) + if (region != 0) + { + if (unusedRegions.contains(region - 1)) + { + const eqg::AreaEnvironment& env = terrain.m_wldAreaEnvironments[region - 1]; + + // skip any region that has no environment info + if (env.type == eqg::AreaEnvironment::Type_None && env.flags == eqg::AreaEnvironment::Flags_None) + { + unusedRegions.erase(region - 1); + // return an empty struct so that we know this wasn't a valid branch + return {}; + } + + if (!info) + { + // not currently in an environment, create a new one and recurse up with the new env set + unusedRegions.erase(region - 1); + Node newNode {{}, 0., region, 0, 0 }; + info.tree.nodes[nodeNum] = newNode; + + // set the env in the return + info.tree.areaNum = terrain.m_wldAreaIndices[region - 1]; + info.envType = env.type; + info.envFlags = env.flags; + + return info; + } + + if (info.envType == env.type && info.envFlags == env.flags) + { + // we have environment info that matches this region's info, so add this node + unusedRegions.erase(region - 1); + Node newNode { {}, 0., region, 0, 0 }; + info.tree.nodes[nodeNum] = newNode; + + return info; + } + + // otherwise, this region doesn't match, so it needs to be saved for later + } + + return {}; + } + + // need to check front first, then pass that result into back if it's non-empty + ExtractInfo frontResult = extractSubtree(front, info); + ExtractInfo backResult = extractSubtree(back, frontResult ? frontResult : info); + + // if neither child has relevant regions, skip this node + if (!frontResult && !backResult) + return {}; + + // at least one child has relevant regions - copy this node + Node newNode { plane.normal, plane.dist, 0, 0, 0 }; + if (frontResult) newNode.front = front; + if (backResult) newNode.back = back; + + // if we have a back result, that means that either there was a front result + // and it was passed into it, or there wasn't and the front result was empty + if (backResult) + { + backResult.tree.nodes[nodeNum] = newNode; + return backResult; + } + + // we must have a front result at this point, so it was the only one that + // returned anything + if (frontResult) + { + frontResult.tree.nodes[nodeNum] = newNode; + return frontResult; + } + } + + return {}; + }; + + while (!unusedRegions.empty()) + { + if (ExtractInfo info = extractSubtree(1, {})) + { + if (!info.tree.nodes.empty()) + { + SPDLOG_DEBUG("Built BSP for env {}:{} with {} nodes", + static_cast(info.envType), static_cast(info.envFlags), info.tree.nodes.size()); + areaTrees.push_back(std::move(info.tree)); + } + else + SPDLOG_WARN("Traversed BSP with no new areas"); + } + } + + SPDLOG_INFO("Built {} area BSP trees from zone BSP tree", areaTrees.size()); + return areaTrees; +} + +#pragma endregion + +//============================================================================================================ +//============================================================================================================ + +#pragma region Polygon Simplification and Triangulation + +//============================================================================================================ +// Polygon Simplification for Convex Hulls -> Triangulated BRep +//============================================================================================================ + +Clipper2Lib::PathsD Triangulate(const Clipper2Lib::PathsD& paths) +{ + Clipper2Lib::PathsD faces; + + if (!paths.empty()) + { + CDT::Triangulation cdt( + CDT::VertexInsertionOrder::Auto, + CDT::IntersectingConstraintEdges::TryResolve, + 1); + + // build edges because we need to account for holes and concavity + std::vector> vertexes; + std::vector edges; + for (const auto& path : paths) + { + vertexes.reserve(vertexes.size() + path.size()); + for (const auto& point : path) + vertexes.emplace_back(static_cast(point.x), static_cast(point.y)); + + auto edgesOffset = static_cast(edges.size()); + edges.reserve(edgesOffset + path.size()); + for (CDT::VertInd i = 0; i < static_cast(path.size()); ++i) + edges.emplace_back(edgesOffset + i, edgesOffset + (i + 1) % static_cast(path.size())); + } + + CDT::RemoveDuplicatesAndRemapEdges(vertexes, edges); + cdt.insertVertices(vertexes); + cdt.insertEdges(edges); + + cdt.eraseOuterTrianglesAndHoles(); + + for (const auto& triangle : cdt.triangles) + { + std::vector points; + points.reserve(triangle.vertices.size() * 2); + for (const auto& vertex : triangle.vertices) + { + points.emplace_back(cdt.vertices[vertex].x); + points.emplace_back(cdt.vertices[vertex].y); + } + + faces.push_back(Clipper2Lib::MakePathD(points)); + } + } + + return faces; +} + +Clipper2Lib::PathsD ExtractPaths(const std::vector& faces, const PlaneBasis& basis) +{ + Clipper2Lib::PathsD paths; + paths.reserve(faces.size()); + for (const auto& face : faces) + { + std::vector points; + points.reserve(face.vertexes.size() * 2); + for (const auto& vertex : face.vertexes) + { + auto projected = basis.project(vertex); + points.emplace_back(projected.x); + points.emplace_back(projected.y); + } + + paths.emplace_back(Clipper2Lib::MakePathD(points)); + } + + return paths; +} + +int FindOrAddVertex(BRep& result, std::vector& vertexPositions, const glm::vec3& pos) +{ + constexpr float VERTEX_MERGE_TOL = 1e-2f; + + for (size_t i = 0; i < vertexPositions.size(); ++i) + if (glm::length(vertexPositions[i] - pos) < VERTEX_MERGE_TOL) + return static_cast(i); + vertexPositions.push_back(pos); + result.addVertex(pos); + return static_cast(vertexPositions.size() - 1); +} + +void AddFacesAndEdges(BRep& result, std::vector& vertexPositions, const Clipper2Lib::PathsD& paths, const Plane& plane, const PlaneBasis& basis) +{ + if (!paths.empty()) + { + auto simplified = Clipper2Lib::SimplifyPaths(paths, 1); + auto triangulated = Triangulate(simplified); + + for (const auto& path : triangulated) + { + if (!path.empty()) + { + std::vector vertexIds; + vertexIds.reserve(path.size()); + for (const auto& point : path) + vertexIds.push_back(FindOrAddVertex(result, vertexPositions, basis.unproject({point.x, point.y}))); + + result.faces[result.addFace(plane)].vertexIds = std::move(vertexIds); + } + } + + for (const auto& path : simplified) + { + if (!path.empty()) + { + auto trimmedPath = Clipper2Lib::TrimCollinear(path, 1); + std::vector vertexIds; + vertexIds.reserve(trimmedPath.size()); + for (const auto& point : trimmedPath) + vertexIds.push_back(FindOrAddVertex(result, vertexPositions, basis.unproject({point.x, point.y}))); + + for (size_t i = 0; i < vertexIds.size(); ++i) + result.outerEdges.emplace_back(vertexIds[i], vertexIds[(i + 1) % vertexIds.size()]); + } + } + } +} + +BRep ConvertFacesByPlane(const std::vector& allFaces) +{ + BRep result; + std::vector vertexPositions; + + // Group faces by coplanar plane (rounded normal + distance) + auto planeKey = [](const Plane& p) { + return std::make_tuple( + static_cast(std::round(p.normal.x * 10)), + static_cast(std::round(p.normal.y * 10)), + static_cast(std::round(p.normal.z * 10)), + static_cast(std::round(p.distance * 1))); + }; + + std::map, std::vector> groups; + for (auto& face : allFaces) + groups[planeKey(face.plane)].push_back(face); + + for (const auto& [key, faces] : groups) + { + PlaneBasis basis = PlaneBasis::fromPlane(faces[0].plane); + auto [x, y, z, d] = key; + std::tuple inverse = {-x, -y, -z, -d}; + if (groups.contains(inverse) && !faces.empty() && !groups[inverse].empty()) + { + // the orientation of the face does not appear to matter to the Clipper2 boolean operations + Clipper2Lib::PathsD inversePaths = ExtractPaths(groups[inverse], basis); + Clipper2Lib::PathsD paths = ExtractPaths(faces, basis);; + + auto diffed = Clipper2Lib::Difference(paths, inversePaths, Clipper2Lib::FillRule::NonZero); + diffed = Clipper2Lib::Union(diffed, Clipper2Lib::FillRule::NonZero); + + AddFacesAndEdges(result, vertexPositions, diffed, faces[0].plane, basis); + } + else if (!faces.empty()) + { + Clipper2Lib::PathsD paths = ExtractPaths(faces, basis); + auto unioned = Clipper2Lib::Union(paths, Clipper2Lib::FillRule::NonZero); + AddFacesAndEdges(result, vertexPositions, unioned, faces[0].plane, basis); + } + } + + return result; +} + +#pragma endregion + + +//============================================================================================================ +//============================================================================================================ + +template Plane ComputeFacePlane(const std::vector& vertices, const T& face); +BRepResult Convert(const std::vector& hulls) +{ + BRepResult result; + + std::vector allCellFaces; + for (const auto& hull : hulls) + { + for (const auto& face : hull.faces) + { + auto plane = ComputeFacePlane(hull.vertices, face); + if (glm::abs(plane.normal.x) > EPSILON || + glm::abs(plane.normal.y) > EPSILON || + glm::abs(plane.normal.z) > EPSILON) + { + std::vector vertexes; + vertexes.reserve(face.size()); + for (const auto& vertexIdx : face) + vertexes.push_back(hull.vertices[vertexIdx]); + + allCellFaces.emplace_back(std::move(vertexes), plane); + } + } + } + + try + { + BRep volume = ConvertFacesByPlane(allCellFaces); + + // convert volume to result + std::transform(volume.vertexes.begin(), volume.vertexes.end(), std::back_inserter(result.vertexes), + [](const Vertex& vert) { return vert.position; }); + + for (const auto& face : volume.faces) + if (face.vertexIds.size() >= 3) // discard any vertexes above 3, triangulation failed? anything less isn't a face + result.faces.emplace_back(std::array{ + static_cast(face.vertexIds[0]), + static_cast(face.vertexIds[1]), + static_cast(face.vertexIds[2])}); + + for (const auto& edge : volume.outerEdges) + result.outerEdges.emplace_back(std::array{ + static_cast(edge.start), + static_cast(edge.end), + }); + } + catch (CDT::Error& e) + { + SPDLOG_ERROR("Error parsing area volume: {}", e.what()); + } + + return result; +} + +std::vector BuildBRepsFromConvexHulls(const std::vector& hulls, const eqg::Terrain& terrain) +{ + std::vector results; + std::map> hullsPerEnv; + + // use a little trick here and select a single area for each individual env type + // this will make lookups later grab the correct env for coloring the area render + std::map, uint32_t> areaTypes; + + for (const auto& hull : hulls) + { + auto area = terrain.m_wldAreaIndices[hull.regionIndex]; + auto env = terrain.m_wldAreaEnvironments[hull.regionIndex]; + + auto [it, inserted] = areaTypes.try_emplace({env.type, env.flags}, area); + hullsPerEnv[it->second].emplace_back(hull); + } + + for (const auto& [area, convexHulls] : hullsPerEnv) + { + auto result = Convert(convexHulls); + result.areaIndex = static_cast(area); + + results.push_back(std::move(result)); + } + + SPDLOG_INFO("Built {} BReps from WLD BSP tree", results.size()); + return results; +} + +//============================================================================================================ +//============================================================================================================ + template <> struct std::hash { @@ -250,7 +884,7 @@ struct std::hash Plane QuantizePlane(const Plane& p) { - return { QuantizeVec3(p.normal, 0.01f), QuantizeFloat(p.distance, 0.01f) }; + return { QuantizeVec3(p.normal, 0.01f), QuantizeFloat(static_cast(p.distance), 0.01f) }; } // Create a Plane suitable for use as a key in an unordered map, both quantized and @@ -282,7 +916,8 @@ struct PlaneHasher }; // Compute plane from a polygon face using Newell's method -Plane ComputeFacePlane(const std::vector& vertices, const std::vector& face) +template +Plane ComputeFacePlane(const std::vector& vertices, const T& face) { Plane plane{ glm::vec3(0.0f), 0.0f }; @@ -310,7 +945,7 @@ Plane ComputeFacePlane(const std::vector& vertices, const std::vector { plane.normal = normal / len; centroid /= static_cast(face.size()); - plane.distance = glm::dot(plane.normal, centroid); + plane.distance = glm::dot(VA::get_triple(plane.normal), centroid); } return plane; @@ -334,7 +969,7 @@ uint32_t GetRandomColor(size_t hashVal) } std::vector DebugColorFacesByPlane(const std::vector& vertices, - const std::vector>& faces) + const std::vector>& faces) { std::vector faceColors(faces.size(), 0xFFFFFFFF); // Default white diff --git a/meshgen/GeometryUtils.h b/meshgen/GeometryUtils.h index e4f8ddce..7a3157a6 100644 --- a/meshgen/GeometryUtils.h +++ b/meshgen/GeometryUtils.h @@ -7,10 +7,12 @@ #include #include #include +#include +#include -// Forward declarations -struct AreaVolumeComponent; +#include "eqglib/eqg_terrain.h" +// Forward declarations namespace eqg { class Terrain; @@ -32,7 +34,7 @@ struct Plane float distance; Plane() - : distance(0.0f) + : distance(0.0) { } @@ -74,7 +76,19 @@ inline glm::vec3 QuantizeVec3(const glm::vec3& v, float gridSize = 1e-4f) // Faces on the same plane (regardless of normal direction) get the same color. // This helps visualize which faces would be candidates for internal face removal. std::vector DebugColorFacesByPlane(const std::vector& vertices, - const std::vector>& faces); + const std::vector>& faces); // Build all convex hulls for regions in areas std::vector BuildConvexHullsFromRegions(const eqg::Terrain& terrain); + +// Result of building a BRep from BSP planes +struct BRepResult +{ + int areaIndex = -1; + + std::vector vertexes; + std::vector> faces; // Polygon faces (triangulated) + std::vector> outerEdges; +}; + +std::vector BuildBRepsFromConvexHulls(const std::vector& hulls, const eqg::Terrain& terrain); diff --git a/meshgen/MeshGenerator.vcxproj b/meshgen/MeshGenerator.vcxproj index ad0e8494..223bfba7 100644 --- a/meshgen/MeshGenerator.vcxproj +++ b/meshgen/MeshGenerator.vcxproj @@ -263,7 +263,7 @@ Windows true - bgfx.lib;bx.lib;bimg.lib;bimg_decode.lib;miniz.lib;fmtd.lib;zlibd.lib;libprotobufd.lib;SDL2-staticd.lib;SDL2maind.lib;d3d11.lib;dxgi.lib;dxguid.lib;imm32.lib;setupapi.lib;winmm.lib;version.lib;%(AdditionalDependencies) + Clipper2.lib;bgfx.lib;bx.lib;bimg.lib;bimg_decode.lib;miniz.lib;fmtd.lib;zlibd.lib;libprotobufd.lib;SDL2-staticd.lib;SDL2maind.lib;d3d11.lib;dxgi.lib;dxguid.lib;imm32.lib;setupapi.lib;winmm.lib;version.lib;%(AdditionalDependencies) false @@ -291,7 +291,7 @@ true true true - bgfx.lib;bx.lib;bimg.lib;bimg_decode.lib;miniz.lib;fmt.lib;zlib.lib;libprotobuf.lib;SDL2-static.lib;SDL2main.lib;d3d11.lib;dxgi.lib;dxguid.lib;imm32.lib;setupapi.lib;winmm.lib;version.lib;%(AdditionalDependencies) + Clipper2.lib;bgfx.lib;bx.lib;bimg.lib;bimg_decode.lib;miniz.lib;fmt.lib;zlib.lib;libprotobuf.lib;SDL2-static.lib;SDL2main.lib;d3d11.lib;dxgi.lib;dxguid.lib;imm32.lib;setupapi.lib;winmm.lib;version.lib;%(AdditionalDependencies) false diff --git a/meshgen/ZoneResourceManager.cpp b/meshgen/ZoneResourceManager.cpp index e4425b6e..fc75f246 100644 --- a/meshgen/ZoneResourceManager.cpp +++ b/meshgen/ZoneResourceManager.cpp @@ -1203,7 +1203,7 @@ void ZoneResourceManager::AddArea(const eqg::TerrainAreaPtr& areaPtr) renderComponent.color = AreaEnvironmentToColor(areaPtr->environment); } -void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) +void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) const { if (!terrain.m_wldBspTree) { @@ -1213,78 +1213,68 @@ void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) SPDLOG_INFO("Generating area volumes from BSP Tree"); auto hulls = BuildConvexHullsFromRegions(terrain); + auto breps = BuildBRepsFromConvexHulls(hulls, terrain); std::unordered_map areaVolumeComponents; std::unordered_map handles; - auto& areaIndices = terrain.m_wldAreaIndices; auto& areas = terrain.m_wldAreas; - auto& environments = terrain.m_wldAreaEnvironments; + auto& environments = terrain.m_wldAreaEnvironmentsPerArea; - for (auto& hull : hulls) + for (auto& brep : breps) { - if (hull.vertices.empty() || hull.faces.empty()) - continue; + if (!brep.vertexes.empty() && !brep.faces.empty()) // TODO: are we okay with no edges? + { + uint32_t areaIndex = brep.areaIndex; + AreaVolumeComponent* areaVolumeComponent; - uint32_t areaIndex = areaIndices[hull.regionIndex]; - AreaVolumeComponent* areaVolumeComponent; - ConvexHullComponent* convexHull = nullptr; + auto iter = areaVolumeComponents.find(areaIndex); + if (iter == areaVolumeComponents.end()) + { + const eqg::SArea* area = &areas[areaIndex]; + entt::handle entity = m_scene->CreateEntity(area->tag); + handles.emplace(areaIndex, entity); - auto iter = areaVolumeComponents.find(areaIndex); - if (iter == areaVolumeComponents.end()) - { - const eqg::SArea* area = &areas[areaIndex]; - entt::handle entity = m_scene->CreateEntity(area->tag); - handles.emplace(areaIndex, entity); - - WldAreaComponent* wldComponent = &entity.emplace(); - wldComponent->environment = environments[hull.regionIndex]; - wldComponent->area = area; - if (wldComponent->environment.hasTeleportEntry) - wldComponent->teleport = terrain.m_teleports[wldComponent->environment.teleportIndex]; - wldComponent->color = AreaEnvironmentToColor(wldComponent->environment); - wldComponent->areaIndex = areaIndex; - - areaVolumeComponent = &entity.emplace(); - - areaVolumeComponents.emplace(areaIndex, areaVolumeComponent); - convexHull = &wldComponent->hulls.emplace_back(); - } - else - { - areaVolumeComponent = iter->second; - convexHull = &handles[areaIndex].get().hulls.emplace_back(); - } + WldAreaComponent* wldComponent = &entity.emplace(); + wldComponent->environment = environments[areaIndex]; + wldComponent->area = area; + if (wldComponent->environment.hasTeleportEntry) + wldComponent->teleport = terrain.m_teleports[wldComponent->environment.teleportIndex]; + wldComponent->color = AreaEnvironmentToColor(wldComponent->environment); + wldComponent->areaIndex = areaIndex; - uint16_t baseIndex = static_cast(areaVolumeComponent->vertices.size()); + areaVolumeComponent = &entity.emplace(); - // Combine vertices & face indices into AreaVolumeComponent - areaVolumeComponent->vertices.reserve(areaVolumeComponent->vertices.size() + hull.vertices.size()); - areaVolumeComponent->vertices.insert(areaVolumeComponent->vertices.end(), hull.vertices.begin(), hull.vertices.end()); - - areaVolumeComponent->faces.reserve(areaVolumeComponent->faces.size() + hull.faces.size()); - for (const auto& face : hull.faces) - { - std::vector adjustedFace; - adjustedFace.reserve(face.size()); - for (uint16_t idx : face) + areaVolumeComponents.emplace(areaIndex, areaVolumeComponent); + } + else { - adjustedFace.push_back(baseIndex + idx); + areaVolumeComponent = iter->second; } - areaVolumeComponent->faces.push_back(std::move(adjustedFace)); - } - // Store triangulated hull for navmesh use - convexHull->vertices = hull.vertices; + auto baseIndex = static_cast(areaVolumeComponent->vertices.size()); - for (const auto& face : hull.faces) - { - // Fan triangulation from first vertex - for (size_t i = 1; i + 1 < face.size(); ++i) + // Combine vertex, face, and (potentially) edge indexes into AreaVolumeComponent + areaVolumeComponent->vertices.reserve(areaVolumeComponent->vertices.size() + brep.vertexes.size()); + areaVolumeComponent->vertices.insert(areaVolumeComponent->vertices.end(), brep.vertexes.begin(), brep.vertexes.end()); + + areaVolumeComponent->faces.reserve(areaVolumeComponent->faces.size() + brep.faces.size()); + for (const auto& face : brep.faces) + { + std::array adjustedFace{}; + adjustedFace[0] = face[0] + baseIndex; + adjustedFace[1] = face[1] + baseIndex; + adjustedFace[2] = face[2] + baseIndex; + areaVolumeComponent->faces.push_back(adjustedFace); + } + + areaVolumeComponent->outerEdges.reserve(areaVolumeComponent->outerEdges.size() + brep.outerEdges.size()); + for (const auto& outerEdge : brep.outerEdges) { - convexHull->indices.push_back(face[0]); - convexHull->indices.push_back(face[i]); - convexHull->indices.push_back(face[i + 1]); + std::array adjustedEdge{}; + adjustedEdge[0] = outerEdge[0] + baseIndex; + adjustedEdge[1] = outerEdge[1] + baseIndex; + areaVolumeComponent->outerEdges.push_back(adjustedEdge); } } } diff --git a/meshgen/ZoneResourceManager.h b/meshgen/ZoneResourceManager.h index 6c7a0008..3dd33360 100644 --- a/meshgen/ZoneResourceManager.h +++ b/meshgen/ZoneResourceManager.h @@ -94,7 +94,7 @@ class ZoneResourceManager void RemovePointLight(const eqg::PointLightPtr& light); void AddArea(const eqg::TerrainAreaPtr& areaPtr); - void CreateWldAreaEntities(const eqg::Terrain& terrain); + void CreateWldAreaEntities(const eqg::Terrain& terrain) const; void AddFace(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3, bool collidable); diff --git a/meshgen/vcpkg_mq.txt b/meshgen/vcpkg_mq.txt index 68772e8c..6a329b56 100644 --- a/meshgen/vcpkg_mq.txt +++ b/meshgen/vcpkg_mq.txt @@ -13,3 +13,5 @@ entt taskflow meshoptimizer yaml-cpp +clipper2 +cdt