From ae520fd8c0a532aae0e4145280482859ba3b59f3 Mon Sep 17 00:00:00 2001 From: dannuic Date: Thu, 5 Feb 2026 12:52:43 -0700 Subject: [PATCH 1/8] Added brep merging of regions --- eqglib/eqg_terrain.cpp | 10 +- eqglib/eqg_terrain.h | 1 + meshgen/GeometryUtils.cpp | 611 ++++++++++++++++++++++++++++++++ meshgen/GeometryUtils.h | 87 +++++ meshgen/ZoneResourceManager.cpp | 123 ++++++- meshgen/ZoneResourceManager.h | 1 + 6 files changed, 827 insertions(+), 6 deletions(-) diff --git a/eqglib/eqg_terrain.cpp b/eqglib/eqg_terrain.cpp index 448de117..d5a7e30b 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); @@ -510,10 +511,11 @@ bool Terrain::InitFromWLDData(const STerrainWLDData& wldData) } } - for (uint32_t regionNum : area.regionNumbers) - { - m_wldAreaEnvironments[regionNum] = env; - } + // for (uint32_t regionNum : area.regionNumbers) + // { + // m_wldAreaEnvironments[regionNum] = env; + // } + m_wldAreaEnvironments[area.areaNum] = env; } } diff --git a/eqglib/eqg_terrain.h b/eqglib/eqg_terrain.h index 041f74c1..43ad353d 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; }; diff --git a/meshgen/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index 64a9c09f..0fd229f0 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -11,6 +11,7 @@ #include "spdlog/spdlog.h" #include +#include #include #include #include @@ -230,6 +231,616 @@ std::vector BuildConvexHullsFromRegions(const eqg::Terrain& te } +#pragma endregion + +#pragma region BRep Merging + + +constexpr double PLANE_THICKNESS = 1e-6; + +// 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::unordered_map BuildAreaBSPTrees(const eqg::Terrain& terrain) +{ + std::unordered_map 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; + + // Build a set of regions for each area for fast lookup + std::unordered_map> areaRegionSets; + for (uint32_t areaNum = 0; areaNum < areas.size(); ++areaNum) + { + if (areaNum < terrain.m_wldAreaIndices.size()) + { + const eqg::AreaEnvironment& env = terrain.m_wldAreaEnvironments[areaNum]; + // Skip regions with no special environment + if (env.type != eqg::AreaEnvironment::Type_None || env.flags != eqg::AreaEnvironment::Flags_None) + for (uint32_t regionNum : areas[areaNum].regionNumbers) + areaRegionSets[areaNum].insert(regionNum); + } + } + + // For each area, extract a subtree + for (auto [areaNum, regionSet] : areaRegionSets) + { + AreaBSPTree areaTree; + areaTree.areaNum = areaNum; + + // Recursive function to determine if a subtree contains any regions + // belonging to this area, and if so, copy the relevant nodes. + // Returns the new node index (1-based) if the subtree was included, 0 otherwise. + std::function extractSubtree; + extractSubtree = [&](uint32_t nodeNum) -> uint32_t + { + 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) + { + // Leaf node - check if region belongs to this area + if (regionSet.contains(region - 1)) + { + // Copy this leaf node + AreaBSPTree::Node newNode; + newNode.region = region - 1; + newNode.front = 0; + newNode.back = 0; + areaTree.nodes[nodeNum] = std::move(newNode); + + return nodeNum; + } + + return 0; + } + + // Internal node - recurse to children + uint32_t frontResult = extractSubtree(front); + uint32_t backResult = extractSubtree(back); + + // If neither child has relevant regions, skip this node + if (frontResult == 0 && backResult == 0) + return 0; + + // At least one child has relevant regions - copy this node + AreaBSPTree::Node newNode; + newNode.normal = plane.normal; + newNode.dist = plane.dist; + newNode.region = 0; + newNode.front = frontResult; + newNode.back = backResult; + areaTree.nodes[nodeNum] = std::move(newNode); + + return nodeNum; + } + + return 0; + }; + + // Start extraction from root (index 1, which is nodes[0]) + areaTree.rootNum = extractSubtree(1); + if (!areaTree.nodes.empty()) + { + SPDLOG_DEBUG("Built BSP tree for area {} with {} nodes (from {} regions)", + areaNum, areaTree.nodes.size(), regionSet.size()); + areaTrees[areaNum] = std::move(areaTree); + } + } + + SPDLOG_INFO("Built {} area BSP trees from zone BSP tree", areaTrees.size()); + return areaTrees; +} + +bool BRepVolume::Segment::isCoincident(const std::vector& boundary) const +{ + return std::any_of(boundary.begin(), boundary.end(), + [this](const Segment& segment) { return isCollinearAndOverlapping(segment); }); +} + +bool BRepVolume::Segment::isCollinearAndOverlapping(const Segment& other) const +{ + auto cross = [](const Vec2& a, const Vec2& b) { return a.x * b.y - a.y * b.x; }; + Vec2 dA = end - start; + + // check collinearity: directions parallel and other.start on a line through this + if (glm::abs(cross(dA, other.end - other.start)) > PLANE_THICKNESS || + glm::abs(cross(dA, start - start)) > PLANE_THICKNESS) + return false; + + double lenA = glm::length(dA); + if (lenA < glm::epsilon()) + return false; + + Vec2 dir = dA / lenA; + double bStart = glm::dot(other.start - start, dir); + double bEnd = glm::dot(other.end - start, dir); + if (bStart > bEnd) + std::swap(bStart, bEnd); + + return glm::min(lenA, bEnd) > glm::max(0., bStart) + glm::epsilon(); +} + +bool isPointInsidePolygon( + const BRepVolume::Vec2& point, + const std::vector& boundary) +{ + using Vec2 = BRepVolume::Vec2; + // ray casting algorithm: count intersections with ray going in +x direction + // using "count lower vertex" rule for consistent vertex handling + // the direction is completely arbitrary, any direction works + int intersections = 0; + for (const auto& seg : boundary) + { + const Vec2& a = seg.start; + const Vec2& b = seg.end; + double minY = glm::min(a.y, b.y); + double maxY = glm::max(a.y, b.y); + + if (point.y >= minY && point.y <= maxY && + glm::abs(b.y - a.y) > glm::epsilon()) // skip horizontal segments + { + double t = (point.y - a.y) / (b.y - a.y); + double xIntersect = a.x + t * (b.x - a.x); + + // count only if crossing upward and intersection is to the right + if (xIntersect > point.x + glm::epsilon() && b.y > a.y) + ++intersections; + } + } + + return intersections % 2 == 1; +} + +BRepVolume::PlaneBasis basisFromPlane(const PolyPlane& plane) +{ + using PlaneBasis = BRepVolume::PlaneBasis; + PlaneBasis basis; + + // compute origin: closest point to the world origin on the plane (really any point will do) + basis.origin = -plane.dist * plane.normal; + + // compute orthonormal basis vectors on the plane + VA::VECTOR up = glm::abs(plane.normal.y) < 0.9 ? VA::VECTOR(0, 1, 0) : VA::VECTOR(1, 0, 0); + basis.u = glm::normalize(glm::cross(plane.normal, up)); + basis.v = glm::cross(plane.normal, basis.u); + + return basis; +} + +BRepVolume::Vec2 BRepVolume::PlaneBasis::project(const Vec3& point) const +{ + Vec3 relative = point - origin; + return Vec2(glm::dot(relative, u), glm::dot(relative, v)); +} + +BRepVolume::Vec3 BRepVolume::PlaneBasis::unproject(const Vec2& point) const +{ + return origin + point.x * u + point.y * v; +} + +std::vector projectFaceToSegments(const BRepVolume::Face& face, const BRepVolume::PlaneBasis& basis) +{ + std::vector segments; + segments.reserve(face.vertexes.size()); + for (size_t i = 0; i < face.vertexes.size(); ++i) + segments.emplace_back(basis.project(face.vertexes[i]), + basis.project(face.vertexes[(i + 1) % face.vertexes.size()])); + + return segments; +} + +std::vector removeCancellingEdges(std::vector segments) +{ + for (size_t i = 0; i < segments.size(); ++i) + { + for (size_t j = i + 1; j < segments.size(); ++j) + { + if (glm::length2(segments[i].start - segments[j].end) < PLANE_THICKNESS * PLANE_THICKNESS && + glm::length2(segments[i].end - segments[j].start) < PLANE_THICKNESS * PLANE_THICKNESS) + { + segments[i] = segments.back(); + segments.pop_back(); + if (j < segments.size()) + { + segments[j] = segments.back(); + segments.pop_back(); + } + --i; + break; + } + } + } + + return segments; +} + +std::optional segmentIntersection( + const BRepVolume::Vec2& a1, const BRepVolume::Vec2& a2, + const BRepVolume::Vec2& b1, const BRepVolume::Vec2& b2) +{ + BRepVolume::Vec2 da = a2 - a1; + BRepVolume::Vec2 db = b2 - b1; + BRepVolume::Vec2 dc = b1 - a1; + + double cross = da.x * db.y - da.y * db.x; + if (glm::abs(cross) < glm::epsilon()) + return {}; + + double t = (dc.x * db.y - dc.y * db.x) / cross; + double s = (dc.x * da.y - dc.y * da.x) / cross; + // t is strictly interior to a, s is anywhere on b + if (t > glm::epsilon() && t < 1. - glm::epsilon() && + s >= -glm::epsilon() && s <= 1. + glm::epsilon()) + return t; + + return {}; +} + +std::vector splitSegmentAtBoundary( + const BRepVolume::Segment& seg, + const std::vector& boundary) +{ + std::vector params = { 0., 1. }; + for (const auto& bSeg : boundary) + if (auto t = segmentIntersection(seg.start, seg.end, bSeg.start, bSeg.end)) + params.push_back(*t); + + std::sort(params.begin(), params.end()); + params.erase(std::unique(params.begin(), params.end(), + [](double a, double b) { return glm::abs(a - b) < glm::epsilon(); }), + params.end()); + + std::vector result; + BRepVolume::Vec2 dir = seg.end - seg.start; + for (size_t i = 0; i + 1 < params.size(); ++i) + if (params[i + 1] - params[i] > glm::epsilon()) + result.emplace_back(seg.start + params[i] * dir, seg.start + params[i + 1] * dir); + + return result; +} + +std::vector classifySegments( + const std::vector& segments, + const std::vector& boundary) +{ + using Seg = BRepVolume::ClassifiedSegment; + using Cls = BRepVolume::ClassifiedSegment::Classification; + + std::vector result; + for (const auto& seg : segments) + { + for (const auto& subSeg : splitSegmentAtBoundary(seg, boundary)) + { + if (subSeg.isCoincident(boundary)) + result.emplace_back(subSeg, Cls::COINCIDENT); + else if (isPointInsidePolygon(subSeg.midpoint(), boundary)) + result.emplace_back(subSeg, Cls::INSIDE); + else + result.emplace_back(subSeg, Cls::OUTSIDE); + } + } + + return result; +} + +// find all disjoint loops from a set of segments +std::vector> findAllLoops(const std::vector& segments) +{ + using Vec2 = BRepVolume::Vec2; + std::vector> loops; + if (segments.empty()) + return loops; + + std::vector used(segments.size(), false); + auto connects = [](const Vec2& a, const Vec2& b) { return glm::length2(a - b) < PLANE_THICKNESS * PLANE_THICKNESS; }; + + for (auto it = std::find(used.begin(), used.end(), false); + it != used.end(); it = std::find(it + 1, used.end(), false)) + { + size_t startIdx = std::distance(used.begin(), it); + + std::vector loop { segments[startIdx].start, segments[startIdx].end }; + used[startIdx] = true; + + bool found = true; + while (found && !connects(loop.back(), loop.front())) + { + found = false; + for (size_t i = 0; !found && i < segments.size(); ++i) + { + if (!used[i]) + { + const auto& seg = segments[i]; + if (connects(seg.start, loop.back())) + { + loop.push_back(seg.end); + used[i] = true; + found = true; + } + else if (connects(seg.end, loop.back())) + { + loop.push_back(seg.start); + used[i] = true; + found = true; + } + } + } + } + + if (connects(loop.front(), loop.back())) loop.pop_back(); + if (loop.size() >= 3) loops.push_back(std::move(loop)); + } + + return loops; +} + +std::vector rebuildFacesFromSegments( + const std::vector& segments, + const BRepVolume::Face& ref, + const BRepVolume::PlaneBasis& basis) +{ + std::vector faces; + for (const auto& loop : findAllLoops(segments)) + { + if (loop.size() >= 3) + { + std::vector vertexes; + vertexes.reserve(loop.size()); + for (const auto& p2d : loop) + vertexes.push_back(basis.unproject(p2d)); + + faces.emplace_back(std::move(vertexes), ref.planeId, ref.areaId); + } + } + + return faces; +} + +// compute difference: primary OUTSIDE + secondary INSIDE (excluding coincident overlaps) +// ie, this is primary - secondary +std::vector computeDifference( + const std::vector& primary, + const std::vector& secondary) +{ + using Seg = BRepVolume::Segment; + using CSeg = BRepVolume::ClassifiedSegment; + + auto isCoincident = [](const Seg& seg, const std::vector& segs) + { + return std::any_of(segs.begin(), segs.end(), [&seg](const CSeg& cs) + { + return cs.classification == CSeg::Classification::COINCIDENT && + seg.isCollinearAndOverlapping(cs); + }); + }; + + std::vector result; + for (const auto& cs : primary) + if (cs.classification == CSeg::Classification::OUTSIDE) + result.push_back(cs); + + for (const auto& cs : secondary) + if (cs.classification == CSeg::Classification::INSIDE && !isCoincident(cs, primary)) + result.push_back(cs); + + return result; +} + +BRepVolume mergeAlongHyperplane( + const BRepVolume& a, + const BRepVolume& b, + const PolyPlane& plane) +{ + using Face = BRepVolume::Face; + using Segment = BRepVolume::Segment; + + BRepVolume result; + std::vector aOnPlane, bOnPlane; + + // Partition faces: copy non-plane faces to result, collect on-plane faces + auto partitionFaces = [&](const BRepVolume& brep, std::vector& onPlane) + { + for (const auto& face : brep.faces) + if (face.valid) + { + if (face.planeId == plane.ID) onPlane.push_back(&face); + else result.faces.push_back(face); + } + }; + + partitionFaces(a, aOnPlane); + partitionFaces(b, bOnPlane); + + // if both sides have faces on the plane, perform the 2D merge + if (!aOnPlane.empty() && !bOnPlane.empty()) + { + BRepVolume::PlaneBasis basis = basisFromPlane(plane); + + // project faces to 2D segments + auto projectAll = [&basis](const std::vector& faces) + { + std::vector segments; + for (const auto* face : faces) + { + auto segs = projectFaceToSegments(*face, basis); + segments.insert(segments.end(), segs.begin(), segs.end()); + } + + return removeCancellingEdges(std::move(segments)); + }; + + auto aSegments = projectAll(aOnPlane); + auto bSegments = projectAll(bOnPlane); + + auto aClassified = classifySegments(aSegments, bSegments); + auto bClassified = classifySegments(bSegments, aSegments); + + // rebuild faces from differences A-B and B-A + auto rebuildAndAdd = [&basis, &result](const std::vector& segs, const Face* ref) + { + for (auto& face : rebuildFacesFromSegments(segs, *ref, basis)) + result.faces.push_back(std::move(face)); + }; + + auto aMinusB = computeDifference(aClassified, bClassified); + auto bMinusA = computeDifference(bClassified, aClassified); + + if (!aMinusB.empty()) rebuildAndAdd(aMinusB, aOnPlane[0]); + if (!bMinusA.empty()) rebuildAndAdd(bMinusA, bOnPlane[0]); + } + else + { + // only one side (or neither) has faces on the plane, keep as-s + for (const auto* face : aOnPlane) result.faces.push_back(*face); + for (const auto* face : bOnPlane) result.faces.push_back(*face); + } + + return result; +} + +BRepVolume BuildBRepFromBSP( + const eqg::Terrain& terrain, + const AreaBSPTree& bspTree, + uint32_t nodeNum, + std::vector& currentPlanes) +{ + if (bspTree.nodes.find(nodeNum) == bspTree.nodes.end()) + return {}; + + const auto& node = bspTree.nodes.at(nodeNum); + + if (node.region != 0) + { + BRepVolume result; + + // the PolyPlane ID is set in the clips member of each poly vertex. Use that to find boundary planes + Polyhedron poly = BuildPolyhedron(terrain.m_aabb.min, terrain.m_aabb.max, currentPlanes); + + // Extract faces as polygons (not triangulated) + std::vector> faces = PolyClipper::extractFaces(poly); + result.faces.reserve(faces.size()); + + for (const auto& faceIndexes : faces) + { + BRepVolume::Face face; + face.areaId = bspTree.areaNum; // TODO: is this - 1? + + // find the plane that contains this face with the intersection of planeIds + std::set planeIds; + if (!faceIndexes.empty()) + { + planeIds.insert(poly[faceIndexes.front()].clips.begin(), poly[faceIndexes.front()].clips.end()); + face.vertexes.reserve(faceIndexes.size()); + for (int idx : faceIndexes) + { + std::set intersection; + std::set_intersection( + poly[idx].clips.begin(), poly[idx].clips.end(), + planeIds.begin(), planeIds.end(), + std::inserter(intersection, intersection.begin())); + + planeIds = std::move(intersection); + face.vertexes.push_back(poly[idx].position); + } + } + else + SPDLOG_ERROR("Empty face for {}", nodeNum); + + + if (planeIds.size() > 0) + face.planeId = *planeIds.begin(); + else + SPDLOG_ERROR("Got 0 intersecting planes for face"); + + if (planeIds.size() > 1) + SPDLOG_WARN("Got more than 1 intersecting planes for face"); + + result.faces.push_back(std::move(face)); + } + + return result; + } + + auto plane = PolyPlane( + node.dist, + VA::Vector(node.normal.x, node.normal.y, node.normal.z), + nodeNum); + + currentPlanes.push_back(plane); + auto frontBRep = BuildBRepFromBSP(terrain, bspTree, node.front, currentPlanes); + currentPlanes.pop_back(); + + plane.normal = -plane.normal; + plane.dist = -plane.dist; + currentPlanes.push_back(plane); + auto backBRep = BuildBRepFromBSP(terrain, bspTree, node.back, currentPlanes); + currentPlanes.pop_back(); + + if (frontBRep.faces.empty()) return backBRep; + if (backBRep.faces.empty()) return frontBRep; + + return mergeAlongHyperplane(frontBRep, backBRep, plane); +} + +std::vector BuildBRepsFromAreas(const eqg::Terrain& terrain) +{ + std::vector results; + + if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) + return results; + + auto areaTrees = BuildAreaBSPTrees(terrain); + for (const auto& [_, areaTree] : areaTrees) + { + std::vector currentPlanes; + auto volume = BuildBRepFromBSP(terrain, areaTree, 1, currentPlanes); + + BRepResult brep; + brep.areaIndex = areaTree.areaNum; + std::vector vertexPositions; + + auto findOrAddVertex = [&vertexPositions, &brep](const BRepVolume::Vec3& pos) + { + for (size_t i = 0; i < vertexPositions.size(); ++i) + if (glm::length2(vertexPositions[i] - pos) < PLANE_THICKNESS * PLANE_THICKNESS) + return static_cast(i); + + vertexPositions.push_back(pos); + brep.vertexes.push_back(pos); + + return static_cast(vertexPositions.size() - 1); + }; + + for (const auto& face : volume.faces) + { + if (face.valid && face.vertexes.size() >= 3) + { + std::vector ids; + ids.reserve(face.vertexes.size()); + for (const auto& vert : face.vertexes) + ids.push_back(findOrAddVertex(vert)); + + brep.faces.push_back(std::move(ids)); + } + } + + results.push_back(brep); + } + + SPDLOG_INFO("Built {} BReps from WLD BSP tree", results.size()); + return results; +} + + #pragma endregion //============================================================================================================ diff --git a/meshgen/GeometryUtils.h b/meshgen/GeometryUtils.h index e4f8ddce..20900044 100644 --- a/meshgen/GeometryUtils.h +++ b/meshgen/GeometryUtils.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include // Forward declarations struct AreaVolumeComponent; @@ -78,3 +80,88 @@ std::vector DebugColorFacesByPlane(const std::vector& verti // Build all convex hulls for regions in areas std::vector BuildConvexHullsFromRegions(const eqg::Terrain& terrain); + +// Result structure for a single area's BSP tree +struct AreaBSPTree +{ + // BSP node for area-specific trees + struct Node + { + glm::vec3 normal; + float dist = 0.f; + uint32_t region; // 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) + }; + + uint32_t areaNum; + uint32_t rootNum; + std::unordered_map nodes; +}; + +struct BRepVolume +{ + using Vec3 = glm::vec<3, double>; + using Vec2 = glm::vec<2, double>; + + struct Face + { + std::vector vertexes; + int planeId; + int areaId = 0; + bool valid = true; + + Face() = default; + Face(std::vector v, int pid, int a = 0) + : vertexes(std::move(v)), planeId(pid), areaId(a) {} + }; + + struct Segment + { + Vec2 start; + Vec2 end; + + [[nodiscard]] Vec2 midpoint() const { return (start + end) * 0.5; } + [[nodiscard]] bool isCoincident(const std::vector& boundary) const; + [[nodiscard]] bool isCollinearAndOverlapping(const Segment& other) const; + }; + + struct ClassifiedSegment : Segment + { + enum class Classification + { + INSIDE, + OUTSIDE, + COINCIDENT + }; + + Classification classification; + }; + + struct PlaneBasis + { + Vec3 origin; // point on the plane + Vec3 u; // first basis vector (tangent) + Vec3 v; // second basis vector (bitangent) + + [[nodiscard]] Vec2 project(const Vec3& point) const; + [[nodiscard]] Vec3 unproject(const Vec2& point) const; + }; + + std::vector faces; +}; + +// Result of building a BRep from BSP planes +struct BRepResult +{ + int areaIndex; + + std::vector vertexes; + std::vector> faces; // Polygon faces (not triangulated) +}; + +// 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 BuildBRepsFromAreas(const eqg::Terrain& terrain); + diff --git a/meshgen/ZoneResourceManager.cpp b/meshgen/ZoneResourceManager.cpp index e4425b6e..30425b65 100644 --- a/meshgen/ZoneResourceManager.cpp +++ b/meshgen/ZoneResourceManager.cpp @@ -191,7 +191,7 @@ bool ZoneResourceManager::BuildScene(Scene& scene) // WLD zones use BSP trees for area bounds if (terrain->m_wldBspTree && !terrain->m_wldAreas.empty()) { - CreateWldAreaEntities(*terrain); + CreateWldAreaEntities2(*terrain); } } @@ -1261,7 +1261,7 @@ void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) // 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) { @@ -1322,3 +1322,122 @@ void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) SPDLOG_INFO("Created {} WLD area entities from BSP tree", areaVolumeComponents.size()); } + +void ZoneResourceManager::CreateWldAreaEntities2(const eqg::Terrain& terrain) +{ + if (!terrain.m_wldBspTree) + { + return; + } + + SPDLOG_INFO("Generating area volumes from BSP Tree"); + + auto breps = BuildBRepsFromAreas(terrain); + + std::unordered_map areaVolumeComponents; + std::unordered_map handles; + + auto& areas = terrain.m_wldAreas; + auto& environments = terrain.m_wldAreaEnvironments; + + for (auto& brep : breps) + { + if (!brep.vertexes.empty() && !brep.faces.empty()) + { + uint32_t areaIndex = brep.areaIndex; + AreaVolumeComponent* areaVolumeComponent; + ConvexHullComponent* convexHull = nullptr; // TODO: this isn't necessarily convex, but reuse the struct + + 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[areaIndex]; + 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(); + } + + uint16_t baseIndex = static_cast(areaVolumeComponent->vertices.size()); + + // Combine vertices & face indices 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::vector adjustedFace; + adjustedFace.reserve(face.size()); + for (uint16_t idx : face) + { + adjustedFace.push_back(baseIndex + idx); + } + areaVolumeComponent->faces.push_back(std::move(adjustedFace)); + } + + // Store triangulated hull for navmesh use + convexHull->vertices = brep.vertexes; + + for (const auto& face : brep.faces) + { + // Fan triangulation from first vertex + for (size_t i = 1; i + 1 < face.size(); ++i) + { + convexHull->indices.push_back(face[0]); + convexHull->indices.push_back(face[i]); + convexHull->indices.push_back(face[i + 1]); + } + } + } + } + + // Now that we have all the convex hulls consolidated, process each area + for (const auto& [areaIndex, areaVolume] : areaVolumeComponents) + { + // Create a transform to give this object local space coordinates. + glm::vec3 center{ 0.0f, 0.0f, 0.0f }; + + // Calculate the center + for (const glm::vec3& vertex : areaVolume->vertices) + { + center += vertex; + } + center /= static_cast(areaVolume->vertices.size()); + + // Center the volume on the new center point + for (glm::vec3& vertex : areaVolume->vertices) + { + vertex -= center; + } + + TransformComponent& transform = handles[areaIndex].get(); + transform.position = center; + + // Add render component with fill color from WldAreaComponent + WldAreaComponent& wldArea = handles[areaIndex].get(); + AreaVolumeRenderComponent& renderComp = handles[areaIndex].emplace(); + + mq::MQColor fillColor = wldArea.color; + fillColor.Alpha = 51; // 20% opacity + renderComp.color = fillColor.ToABGR(); + } + + SPDLOG_INFO("Created {} WLD area entities from BSP tree", areaVolumeComponents.size()); +} diff --git a/meshgen/ZoneResourceManager.h b/meshgen/ZoneResourceManager.h index 6c7a0008..96ffc325 100644 --- a/meshgen/ZoneResourceManager.h +++ b/meshgen/ZoneResourceManager.h @@ -95,6 +95,7 @@ class ZoneResourceManager void AddArea(const eqg::TerrainAreaPtr& areaPtr); void CreateWldAreaEntities(const eqg::Terrain& terrain); + void CreateWldAreaEntities2(const eqg::Terrain& terrain); void AddFace(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3, bool collidable); From 554ed8e99fdaae440030c820c85b9be6439baa90 Mon Sep 17 00:00:00 2001 From: dannuic Date: Thu, 5 Feb 2026 13:29:13 -0700 Subject: [PATCH 2/8] separated area env index to prevent potential area resolution issues --- eqglib/eqg_terrain.cpp | 11 ++++++----- eqglib/eqg_terrain.h | 1 + meshgen/GeometryUtils.cpp | 2 +- meshgen/ZoneResourceManager.cpp | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/eqglib/eqg_terrain.cpp b/eqglib/eqg_terrain.cpp index d5a7e30b..d45ccf17 100644 --- a/eqglib/eqg_terrain.cpp +++ b/eqglib/eqg_terrain.cpp @@ -461,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) { @@ -511,11 +512,11 @@ bool Terrain::InitFromWLDData(const STerrainWLDData& wldData) } } - // for (uint32_t regionNum : area.regionNumbers) - // { - // m_wldAreaEnvironments[regionNum] = env; - // } - m_wldAreaEnvironments[area.areaNum] = env; + 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 43ad353d..7308eefd 100644 --- a/eqglib/eqg_terrain.h +++ b/eqglib/eqg_terrain.h @@ -149,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/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index 0fd229f0..af30df8b 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -260,7 +260,7 @@ std::unordered_map BuildAreaBSPTrees(const eqg::Terrain& { if (areaNum < terrain.m_wldAreaIndices.size()) { - const eqg::AreaEnvironment& env = terrain.m_wldAreaEnvironments[areaNum]; + const eqg::AreaEnvironment& env = terrain.m_wldAreaEnvironmentsPerArea[areaNum]; // Skip regions with no special environment if (env.type != eqg::AreaEnvironment::Type_None || env.flags != eqg::AreaEnvironment::Flags_None) for (uint32_t regionNum : areas[areaNum].regionNumbers) diff --git a/meshgen/ZoneResourceManager.cpp b/meshgen/ZoneResourceManager.cpp index 30425b65..4e372f56 100644 --- a/meshgen/ZoneResourceManager.cpp +++ b/meshgen/ZoneResourceManager.cpp @@ -1338,7 +1338,7 @@ void ZoneResourceManager::CreateWldAreaEntities2(const eqg::Terrain& terrain) std::unordered_map handles; auto& areas = terrain.m_wldAreas; - auto& environments = terrain.m_wldAreaEnvironments; + auto& environments = terrain.m_wldAreaEnvironmentsPerArea; for (auto& brep : breps) { From 0f4f94fe522e0063883e37f71d22262058c72a0c Mon Sep 17 00:00:00 2001 From: dannuic Date: Fri, 13 Feb 2026 13:10:02 -0700 Subject: [PATCH 3/8] Pulled in libraries to make code cleaner, code builds triangulated faces now --- meshgen/BRepConverter.cpp | 731 ++++++++++++++++++++++++++ meshgen/GeometryUtils.cpp | 614 +-------------------- meshgen/GeometryUtils.h | 90 +--- meshgen/MeshGenerator.vcxproj | 5 +- meshgen/MeshGenerator.vcxproj.filters | 3 + meshgen/ZoneResourceManager.cpp | 2 +- meshgen/vcpkg_mq.txt | 2 + 7 files changed, 752 insertions(+), 695 deletions(-) create mode 100644 meshgen/BRepConverter.cpp diff --git a/meshgen/BRepConverter.cpp b/meshgen/BRepConverter.cpp new file mode 100644 index 00000000..37d84b5c --- /dev/null +++ b/meshgen/BRepConverter.cpp @@ -0,0 +1,731 @@ +// +// Created by dannu on 2/9/2026. +// + +#include "meshgen/GeometryUtils.h" + +#include "eqglib/eqg_terrain.h" +#include "spdlog/spdlog.h" +#include +#include +#include "triangle.h" +#include "CDT.h" + +#include +#include +#include +#include +#include +#include +#include + +using Distance = float; +using Vec3 = glm::vec<3, Distance>; +using Vec2 = glm::vec<2, Distance>; + +constexpr Distance EPSILON = glm::epsilon(); +constexpr Distance PLANE_THICKNESS = static_cast(1e-6); + +// Orthonormal basis for projecting 3D points onto a plane +struct PlaneBasis +{ + Vec3 origin; // A point on the plane + Vec3 u; // First basis vector (tangent) + 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 + Vec3 up = glm::abs(plane.normal.y) < 0.9 ? Vec3(0, 1, 0) : 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]] Vec2 project(const Vec3& point) const + { + Vec3 relative = point - origin; + return {glm::dot(relative, u), glm::dot(relative, v)}; + } + + // Unproject 2D coordinates back to 3D point on the plane + [[nodiscard]] Vec3 unproject(const Vec2& point) const + { + return origin + point.x * u + point.y * v; + } +}; + +// BSP node for area-specific trees +struct Node +{ + Vec3 normal; + Distance 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 +{ + Vec3 position; + int halfEdge = -1; // one of the outgoing half edges for traversal + int id = -1; + + Vertex() = default; + explicit Vertex(const Vec3& position, int id) + : position(position), id(id) {} +}; + +struct Face +{ + std::vector vertexes; + std::vector vertexIds; + Plane plane; + int id = -1; + + Face() = default; + explicit Face(std::vector vertexes, const Plane& plane, int id) + : vertexes(std::move(vertexes)), plane(plane), id(id) {} + + explicit Face(const Plane& plane, int id) + : plane(plane), id(id) {} +}; + +struct BRep +{ + + std::vector vertexes; + std::vector faces; + + int addVertex(const 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; + } +}; + + +#pragma region BSP Debugging Functions + + +// 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 Volume Simplification + + +struct MergeFace +{ + std::vector vertexes; // CCW order when viewed from normal direction + Plane plane; + bool valid = true; + + MergeFace() = default; + MergeFace(std::vector verts, const Plane& p) + : vertexes(std::move(verts)), plane(p) {} +}; + +struct Segment +{ + Vec3 start; + Vec3 end; +}; + +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; +} + +BRep groupFaces(const std::vector& allFaces) +{ + // 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) + if (face.valid) + groups[planeKey(face.plane)].push_back(face); + + std::map, std::vector> test; + for (const auto& [key, faces] : groups) + { + PlaneBasis basis = PlaneBasis::fromPlane(faces[0].plane); + test[key].reserve(faces.size()); + for (const auto& face : faces) + { + std::vector vertexes; + vertexes.reserve(face.vertexes.size()); + for (const auto& vertex : face.vertexes) + { + auto projected = basis.project(vertex); + auto unprojected = basis.unproject(projected); + vertexes.emplace_back(unprojected); + } + + test[key].emplace_back(vertexes, face.plane); + } + } + + std::map, std::vector> xoredFaces; + 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}; // TODO: might need to check +/- 1 for all of these + if (groups.contains(inverse)) // can assume non-empty here + { + auto inverseFaces = groups[inverse]; + Clipper2Lib::PathsD inversePaths; + inversePaths.reserve(inverseFaces.size()); + // the orientation of the face does not appear to matter to the Clipper2 boolean operations + for (const auto& face : inverseFaces) + { + std::vector inversePoints; + inversePoints.reserve(face.vertexes.size() * 2); + for (const auto& vertex : face.vertexes) + { + auto projected = basis.project(vertex); + inversePoints.emplace_back(projected.x); + inversePoints.emplace_back(projected.y); + } + + inversePaths.emplace_back(Clipper2Lib::MakePathD(inversePoints)); + } + + 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)); + } + + auto diffed = Clipper2Lib::Difference(paths, inversePaths, Clipper2Lib::FillRule::NonZero); + diffed = Clipper2Lib::Union(diffed, Clipper2Lib::FillRule::NonZero); + diffed = Clipper2Lib::SimplifyPaths(diffed, 1); + + Clipper2Lib::PathsD solution = triangulate(diffed); + //Clipper2Lib::Triangulate(diffed, 0, solution); + solution = Clipper2Lib::SimplifyPaths(solution, 1); + + if (!solution.empty()) + { + xoredFaces[key].reserve(solution.size()); + for (const auto& path : solution) + { + if (!path.empty()) + { + MergeFace newFace; + newFace.plane = faces[0].plane; + newFace.vertexes.reserve(path.size()); + for (const auto& point : path) + newFace.vertexes.push_back(basis.unproject({point.x, point.y})); + + xoredFaces[key].push_back(newFace); + } + } + } + } + else if (!faces.empty()) + { + 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)); + } + + auto unioned = Clipper2Lib::Union(paths, Clipper2Lib::FillRule::NonZero); + unioned = Clipper2Lib::SimplifyPaths(unioned, 1); + + Clipper2Lib::PathsD solution = triangulate(unioned); + // Clipper2Lib::Triangulate(unioned, 0, solution); + solution = Clipper2Lib::SimplifyPaths(solution, 1); + + if (!solution.empty()) + { + xoredFaces[key].reserve(solution.size()); + for (const auto& path : solution) + { + if (!path.empty()) + { + MergeFace newFace; + newFace.plane = faces[0].plane; + newFace.vertexes.reserve(path.size()); + for (const auto& point : path) + newFace.vertexes.push_back(basis.unproject({point.x, point.y})); + + xoredFaces[key].push_back(newFace); + } + } + } + } + } + + BRep result; + constexpr Distance VERTEX_MERGE_TOL = 1e-2f; + std::vector vertexPositions; + + auto findOrAddVertex = [&result, &vertexPositions](const Vec3& pos) -> int + { + 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); + }; + + for (const auto& [key, faces] : xoredFaces) + { + for (const auto& face : faces) + { + std::vector vertexIds; + for (const auto& vertex : face.vertexes) + vertexIds.push_back(findOrAddVertex(vertex)); + + int faceId = result.addFace(face.plane); + result.faces[faceId].vertexIds = std::move(vertexIds); + } + } + + return result; +} + +#pragma endregion + + +#pragma region Polyhedra Union + +class PolyhedraUnionConverter +{ +public: + static BRepResult convert(const std::vector& hulls); +}; + +// --- Main conversion --- + +BRepResult PolyhedraUnionConverter::convert(const std::vector& hulls) +{ + BRepResult result; + + std::vector allCellFaces; + for (const auto& hull : hulls) + { + std::vector hullVertexes; + hullVertexes.reserve(hull.vertices.size()); + for (const auto& vertex : hull.vertices) + hullVertexes.emplace_back(vertex.x, vertex.y, vertex.z); + + for (const auto& face : hull.faces) + { + if (face.size() >= 3) + { + std::vector vertexes; + vertexes.reserve(face.size()); + for (const auto& vertexIdx : face) + vertexes.push_back(hullVertexes[vertexIdx]); + + // Use Newell's method to compute robust normal for arbitrary polygon + Vec3 normal; + Vec3 centroid; + for (size_t i = 0; i < vertexes.size(); ++i) + { + const Vec3& current = vertexes[i]; + const Vec3& next = vertexes[(i + 1) % vertexes.size()]; + + normal.x += (current.y - next.y) * (current.z + next.z); + normal.y += (current.z - next.z) * (current.x + next.x); + normal.z += (current.x - next.x) * (current.y + next.y); + + centroid += current; + } + + Distance len = glm::length(normal); + if (len > EPSILON) + { + Plane plane; + plane.normal = normal / len; + centroid /= static_cast(vertexes.size()); + plane.distance = glm::dot(plane.normal, centroid); + + allCellFaces.emplace_back(vertexes, plane); + } + } + } + } + + try + { + BRep volume = groupFaces(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) + result.faces.emplace_back(face.vertexIds.begin(), face.vertexIds.end()); + } + catch (CDT::Error& e) + { + SPDLOG_ERROR(e.what()); + } + + return result; +} + +std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain) +{ + std::vector results; + + if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) + return results; + + auto hulls = BuildConvexHullsFromRegions(terrain); + std::map> convexHulls; + 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); + convexHulls[it->second].emplace_back(hull); + } + + for (const auto& [area, convexHulls] : convexHulls) + { + auto result = PolyhedraUnionConverter::convert(convexHulls); + result.areaIndex = area; + saveOBJ(result, fmt::format("test_{}.obj", area)); + + results.push_back(std::move(result)); + } + + SPDLOG_INFO("Built {} BReps from WLD BSP tree", results.size()); + return results; +} + + +#pragma endregion diff --git a/meshgen/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index af30df8b..fb1f0616 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -231,616 +231,6 @@ std::vector BuildConvexHullsFromRegions(const eqg::Terrain& te } -#pragma endregion - -#pragma region BRep Merging - - -constexpr double PLANE_THICKNESS = 1e-6; - -// 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::unordered_map BuildAreaBSPTrees(const eqg::Terrain& terrain) -{ - std::unordered_map 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; - - // Build a set of regions for each area for fast lookup - std::unordered_map> areaRegionSets; - for (uint32_t areaNum = 0; areaNum < areas.size(); ++areaNum) - { - if (areaNum < terrain.m_wldAreaIndices.size()) - { - const eqg::AreaEnvironment& env = terrain.m_wldAreaEnvironmentsPerArea[areaNum]; - // Skip regions with no special environment - if (env.type != eqg::AreaEnvironment::Type_None || env.flags != eqg::AreaEnvironment::Flags_None) - for (uint32_t regionNum : areas[areaNum].regionNumbers) - areaRegionSets[areaNum].insert(regionNum); - } - } - - // For each area, extract a subtree - for (auto [areaNum, regionSet] : areaRegionSets) - { - AreaBSPTree areaTree; - areaTree.areaNum = areaNum; - - // Recursive function to determine if a subtree contains any regions - // belonging to this area, and if so, copy the relevant nodes. - // Returns the new node index (1-based) if the subtree was included, 0 otherwise. - std::function extractSubtree; - extractSubtree = [&](uint32_t nodeNum) -> uint32_t - { - 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) - { - // Leaf node - check if region belongs to this area - if (regionSet.contains(region - 1)) - { - // Copy this leaf node - AreaBSPTree::Node newNode; - newNode.region = region - 1; - newNode.front = 0; - newNode.back = 0; - areaTree.nodes[nodeNum] = std::move(newNode); - - return nodeNum; - } - - return 0; - } - - // Internal node - recurse to children - uint32_t frontResult = extractSubtree(front); - uint32_t backResult = extractSubtree(back); - - // If neither child has relevant regions, skip this node - if (frontResult == 0 && backResult == 0) - return 0; - - // At least one child has relevant regions - copy this node - AreaBSPTree::Node newNode; - newNode.normal = plane.normal; - newNode.dist = plane.dist; - newNode.region = 0; - newNode.front = frontResult; - newNode.back = backResult; - areaTree.nodes[nodeNum] = std::move(newNode); - - return nodeNum; - } - - return 0; - }; - - // Start extraction from root (index 1, which is nodes[0]) - areaTree.rootNum = extractSubtree(1); - if (!areaTree.nodes.empty()) - { - SPDLOG_DEBUG("Built BSP tree for area {} with {} nodes (from {} regions)", - areaNum, areaTree.nodes.size(), regionSet.size()); - areaTrees[areaNum] = std::move(areaTree); - } - } - - SPDLOG_INFO("Built {} area BSP trees from zone BSP tree", areaTrees.size()); - return areaTrees; -} - -bool BRepVolume::Segment::isCoincident(const std::vector& boundary) const -{ - return std::any_of(boundary.begin(), boundary.end(), - [this](const Segment& segment) { return isCollinearAndOverlapping(segment); }); -} - -bool BRepVolume::Segment::isCollinearAndOverlapping(const Segment& other) const -{ - auto cross = [](const Vec2& a, const Vec2& b) { return a.x * b.y - a.y * b.x; }; - Vec2 dA = end - start; - - // check collinearity: directions parallel and other.start on a line through this - if (glm::abs(cross(dA, other.end - other.start)) > PLANE_THICKNESS || - glm::abs(cross(dA, start - start)) > PLANE_THICKNESS) - return false; - - double lenA = glm::length(dA); - if (lenA < glm::epsilon()) - return false; - - Vec2 dir = dA / lenA; - double bStart = glm::dot(other.start - start, dir); - double bEnd = glm::dot(other.end - start, dir); - if (bStart > bEnd) - std::swap(bStart, bEnd); - - return glm::min(lenA, bEnd) > glm::max(0., bStart) + glm::epsilon(); -} - -bool isPointInsidePolygon( - const BRepVolume::Vec2& point, - const std::vector& boundary) -{ - using Vec2 = BRepVolume::Vec2; - // ray casting algorithm: count intersections with ray going in +x direction - // using "count lower vertex" rule for consistent vertex handling - // the direction is completely arbitrary, any direction works - int intersections = 0; - for (const auto& seg : boundary) - { - const Vec2& a = seg.start; - const Vec2& b = seg.end; - double minY = glm::min(a.y, b.y); - double maxY = glm::max(a.y, b.y); - - if (point.y >= minY && point.y <= maxY && - glm::abs(b.y - a.y) > glm::epsilon()) // skip horizontal segments - { - double t = (point.y - a.y) / (b.y - a.y); - double xIntersect = a.x + t * (b.x - a.x); - - // count only if crossing upward and intersection is to the right - if (xIntersect > point.x + glm::epsilon() && b.y > a.y) - ++intersections; - } - } - - return intersections % 2 == 1; -} - -BRepVolume::PlaneBasis basisFromPlane(const PolyPlane& plane) -{ - using PlaneBasis = BRepVolume::PlaneBasis; - PlaneBasis basis; - - // compute origin: closest point to the world origin on the plane (really any point will do) - basis.origin = -plane.dist * plane.normal; - - // compute orthonormal basis vectors on the plane - VA::VECTOR up = glm::abs(plane.normal.y) < 0.9 ? VA::VECTOR(0, 1, 0) : VA::VECTOR(1, 0, 0); - basis.u = glm::normalize(glm::cross(plane.normal, up)); - basis.v = glm::cross(plane.normal, basis.u); - - return basis; -} - -BRepVolume::Vec2 BRepVolume::PlaneBasis::project(const Vec3& point) const -{ - Vec3 relative = point - origin; - return Vec2(glm::dot(relative, u), glm::dot(relative, v)); -} - -BRepVolume::Vec3 BRepVolume::PlaneBasis::unproject(const Vec2& point) const -{ - return origin + point.x * u + point.y * v; -} - -std::vector projectFaceToSegments(const BRepVolume::Face& face, const BRepVolume::PlaneBasis& basis) -{ - std::vector segments; - segments.reserve(face.vertexes.size()); - for (size_t i = 0; i < face.vertexes.size(); ++i) - segments.emplace_back(basis.project(face.vertexes[i]), - basis.project(face.vertexes[(i + 1) % face.vertexes.size()])); - - return segments; -} - -std::vector removeCancellingEdges(std::vector segments) -{ - for (size_t i = 0; i < segments.size(); ++i) - { - for (size_t j = i + 1; j < segments.size(); ++j) - { - if (glm::length2(segments[i].start - segments[j].end) < PLANE_THICKNESS * PLANE_THICKNESS && - glm::length2(segments[i].end - segments[j].start) < PLANE_THICKNESS * PLANE_THICKNESS) - { - segments[i] = segments.back(); - segments.pop_back(); - if (j < segments.size()) - { - segments[j] = segments.back(); - segments.pop_back(); - } - --i; - break; - } - } - } - - return segments; -} - -std::optional segmentIntersection( - const BRepVolume::Vec2& a1, const BRepVolume::Vec2& a2, - const BRepVolume::Vec2& b1, const BRepVolume::Vec2& b2) -{ - BRepVolume::Vec2 da = a2 - a1; - BRepVolume::Vec2 db = b2 - b1; - BRepVolume::Vec2 dc = b1 - a1; - - double cross = da.x * db.y - da.y * db.x; - if (glm::abs(cross) < glm::epsilon()) - return {}; - - double t = (dc.x * db.y - dc.y * db.x) / cross; - double s = (dc.x * da.y - dc.y * da.x) / cross; - // t is strictly interior to a, s is anywhere on b - if (t > glm::epsilon() && t < 1. - glm::epsilon() && - s >= -glm::epsilon() && s <= 1. + glm::epsilon()) - return t; - - return {}; -} - -std::vector splitSegmentAtBoundary( - const BRepVolume::Segment& seg, - const std::vector& boundary) -{ - std::vector params = { 0., 1. }; - for (const auto& bSeg : boundary) - if (auto t = segmentIntersection(seg.start, seg.end, bSeg.start, bSeg.end)) - params.push_back(*t); - - std::sort(params.begin(), params.end()); - params.erase(std::unique(params.begin(), params.end(), - [](double a, double b) { return glm::abs(a - b) < glm::epsilon(); }), - params.end()); - - std::vector result; - BRepVolume::Vec2 dir = seg.end - seg.start; - for (size_t i = 0; i + 1 < params.size(); ++i) - if (params[i + 1] - params[i] > glm::epsilon()) - result.emplace_back(seg.start + params[i] * dir, seg.start + params[i + 1] * dir); - - return result; -} - -std::vector classifySegments( - const std::vector& segments, - const std::vector& boundary) -{ - using Seg = BRepVolume::ClassifiedSegment; - using Cls = BRepVolume::ClassifiedSegment::Classification; - - std::vector result; - for (const auto& seg : segments) - { - for (const auto& subSeg : splitSegmentAtBoundary(seg, boundary)) - { - if (subSeg.isCoincident(boundary)) - result.emplace_back(subSeg, Cls::COINCIDENT); - else if (isPointInsidePolygon(subSeg.midpoint(), boundary)) - result.emplace_back(subSeg, Cls::INSIDE); - else - result.emplace_back(subSeg, Cls::OUTSIDE); - } - } - - return result; -} - -// find all disjoint loops from a set of segments -std::vector> findAllLoops(const std::vector& segments) -{ - using Vec2 = BRepVolume::Vec2; - std::vector> loops; - if (segments.empty()) - return loops; - - std::vector used(segments.size(), false); - auto connects = [](const Vec2& a, const Vec2& b) { return glm::length2(a - b) < PLANE_THICKNESS * PLANE_THICKNESS; }; - - for (auto it = std::find(used.begin(), used.end(), false); - it != used.end(); it = std::find(it + 1, used.end(), false)) - { - size_t startIdx = std::distance(used.begin(), it); - - std::vector loop { segments[startIdx].start, segments[startIdx].end }; - used[startIdx] = true; - - bool found = true; - while (found && !connects(loop.back(), loop.front())) - { - found = false; - for (size_t i = 0; !found && i < segments.size(); ++i) - { - if (!used[i]) - { - const auto& seg = segments[i]; - if (connects(seg.start, loop.back())) - { - loop.push_back(seg.end); - used[i] = true; - found = true; - } - else if (connects(seg.end, loop.back())) - { - loop.push_back(seg.start); - used[i] = true; - found = true; - } - } - } - } - - if (connects(loop.front(), loop.back())) loop.pop_back(); - if (loop.size() >= 3) loops.push_back(std::move(loop)); - } - - return loops; -} - -std::vector rebuildFacesFromSegments( - const std::vector& segments, - const BRepVolume::Face& ref, - const BRepVolume::PlaneBasis& basis) -{ - std::vector faces; - for (const auto& loop : findAllLoops(segments)) - { - if (loop.size() >= 3) - { - std::vector vertexes; - vertexes.reserve(loop.size()); - for (const auto& p2d : loop) - vertexes.push_back(basis.unproject(p2d)); - - faces.emplace_back(std::move(vertexes), ref.planeId, ref.areaId); - } - } - - return faces; -} - -// compute difference: primary OUTSIDE + secondary INSIDE (excluding coincident overlaps) -// ie, this is primary - secondary -std::vector computeDifference( - const std::vector& primary, - const std::vector& secondary) -{ - using Seg = BRepVolume::Segment; - using CSeg = BRepVolume::ClassifiedSegment; - - auto isCoincident = [](const Seg& seg, const std::vector& segs) - { - return std::any_of(segs.begin(), segs.end(), [&seg](const CSeg& cs) - { - return cs.classification == CSeg::Classification::COINCIDENT && - seg.isCollinearAndOverlapping(cs); - }); - }; - - std::vector result; - for (const auto& cs : primary) - if (cs.classification == CSeg::Classification::OUTSIDE) - result.push_back(cs); - - for (const auto& cs : secondary) - if (cs.classification == CSeg::Classification::INSIDE && !isCoincident(cs, primary)) - result.push_back(cs); - - return result; -} - -BRepVolume mergeAlongHyperplane( - const BRepVolume& a, - const BRepVolume& b, - const PolyPlane& plane) -{ - using Face = BRepVolume::Face; - using Segment = BRepVolume::Segment; - - BRepVolume result; - std::vector aOnPlane, bOnPlane; - - // Partition faces: copy non-plane faces to result, collect on-plane faces - auto partitionFaces = [&](const BRepVolume& brep, std::vector& onPlane) - { - for (const auto& face : brep.faces) - if (face.valid) - { - if (face.planeId == plane.ID) onPlane.push_back(&face); - else result.faces.push_back(face); - } - }; - - partitionFaces(a, aOnPlane); - partitionFaces(b, bOnPlane); - - // if both sides have faces on the plane, perform the 2D merge - if (!aOnPlane.empty() && !bOnPlane.empty()) - { - BRepVolume::PlaneBasis basis = basisFromPlane(plane); - - // project faces to 2D segments - auto projectAll = [&basis](const std::vector& faces) - { - std::vector segments; - for (const auto* face : faces) - { - auto segs = projectFaceToSegments(*face, basis); - segments.insert(segments.end(), segs.begin(), segs.end()); - } - - return removeCancellingEdges(std::move(segments)); - }; - - auto aSegments = projectAll(aOnPlane); - auto bSegments = projectAll(bOnPlane); - - auto aClassified = classifySegments(aSegments, bSegments); - auto bClassified = classifySegments(bSegments, aSegments); - - // rebuild faces from differences A-B and B-A - auto rebuildAndAdd = [&basis, &result](const std::vector& segs, const Face* ref) - { - for (auto& face : rebuildFacesFromSegments(segs, *ref, basis)) - result.faces.push_back(std::move(face)); - }; - - auto aMinusB = computeDifference(aClassified, bClassified); - auto bMinusA = computeDifference(bClassified, aClassified); - - if (!aMinusB.empty()) rebuildAndAdd(aMinusB, aOnPlane[0]); - if (!bMinusA.empty()) rebuildAndAdd(bMinusA, bOnPlane[0]); - } - else - { - // only one side (or neither) has faces on the plane, keep as-s - for (const auto* face : aOnPlane) result.faces.push_back(*face); - for (const auto* face : bOnPlane) result.faces.push_back(*face); - } - - return result; -} - -BRepVolume BuildBRepFromBSP( - const eqg::Terrain& terrain, - const AreaBSPTree& bspTree, - uint32_t nodeNum, - std::vector& currentPlanes) -{ - if (bspTree.nodes.find(nodeNum) == bspTree.nodes.end()) - return {}; - - const auto& node = bspTree.nodes.at(nodeNum); - - if (node.region != 0) - { - BRepVolume result; - - // the PolyPlane ID is set in the clips member of each poly vertex. Use that to find boundary planes - Polyhedron poly = BuildPolyhedron(terrain.m_aabb.min, terrain.m_aabb.max, currentPlanes); - - // Extract faces as polygons (not triangulated) - std::vector> faces = PolyClipper::extractFaces(poly); - result.faces.reserve(faces.size()); - - for (const auto& faceIndexes : faces) - { - BRepVolume::Face face; - face.areaId = bspTree.areaNum; // TODO: is this - 1? - - // find the plane that contains this face with the intersection of planeIds - std::set planeIds; - if (!faceIndexes.empty()) - { - planeIds.insert(poly[faceIndexes.front()].clips.begin(), poly[faceIndexes.front()].clips.end()); - face.vertexes.reserve(faceIndexes.size()); - for (int idx : faceIndexes) - { - std::set intersection; - std::set_intersection( - poly[idx].clips.begin(), poly[idx].clips.end(), - planeIds.begin(), planeIds.end(), - std::inserter(intersection, intersection.begin())); - - planeIds = std::move(intersection); - face.vertexes.push_back(poly[idx].position); - } - } - else - SPDLOG_ERROR("Empty face for {}", nodeNum); - - - if (planeIds.size() > 0) - face.planeId = *planeIds.begin(); - else - SPDLOG_ERROR("Got 0 intersecting planes for face"); - - if (planeIds.size() > 1) - SPDLOG_WARN("Got more than 1 intersecting planes for face"); - - result.faces.push_back(std::move(face)); - } - - return result; - } - - auto plane = PolyPlane( - node.dist, - VA::Vector(node.normal.x, node.normal.y, node.normal.z), - nodeNum); - - currentPlanes.push_back(plane); - auto frontBRep = BuildBRepFromBSP(terrain, bspTree, node.front, currentPlanes); - currentPlanes.pop_back(); - - plane.normal = -plane.normal; - plane.dist = -plane.dist; - currentPlanes.push_back(plane); - auto backBRep = BuildBRepFromBSP(terrain, bspTree, node.back, currentPlanes); - currentPlanes.pop_back(); - - if (frontBRep.faces.empty()) return backBRep; - if (backBRep.faces.empty()) return frontBRep; - - return mergeAlongHyperplane(frontBRep, backBRep, plane); -} - -std::vector BuildBRepsFromAreas(const eqg::Terrain& terrain) -{ - std::vector results; - - if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) - return results; - - auto areaTrees = BuildAreaBSPTrees(terrain); - for (const auto& [_, areaTree] : areaTrees) - { - std::vector currentPlanes; - auto volume = BuildBRepFromBSP(terrain, areaTree, 1, currentPlanes); - - BRepResult brep; - brep.areaIndex = areaTree.areaNum; - std::vector vertexPositions; - - auto findOrAddVertex = [&vertexPositions, &brep](const BRepVolume::Vec3& pos) - { - for (size_t i = 0; i < vertexPositions.size(); ++i) - if (glm::length2(vertexPositions[i] - pos) < PLANE_THICKNESS * PLANE_THICKNESS) - return static_cast(i); - - vertexPositions.push_back(pos); - brep.vertexes.push_back(pos); - - return static_cast(vertexPositions.size() - 1); - }; - - for (const auto& face : volume.faces) - { - if (face.valid && face.vertexes.size() >= 3) - { - std::vector ids; - ids.reserve(face.vertexes.size()); - for (const auto& vert : face.vertexes) - ids.push_back(findOrAddVertex(vert)); - - brep.faces.push_back(std::move(ids)); - } - } - - results.push_back(brep); - } - - SPDLOG_INFO("Built {} BReps from WLD BSP tree", results.size()); - return results; -} - - #pragma endregion //============================================================================================================ @@ -861,7 +251,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 @@ -921,7 +311,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; diff --git a/meshgen/GeometryUtils.h b/meshgen/GeometryUtils.h index 20900044..8560de0a 100644 --- a/meshgen/GeometryUtils.h +++ b/meshgen/GeometryUtils.h @@ -10,6 +10,8 @@ #include #include +#include "eqglib/eqg_terrain.h" + // Forward declarations struct AreaVolumeComponent; @@ -30,15 +32,17 @@ struct ConvexHullResult // Simple plane representation struct Plane { - glm::vec3 normal; - float distance; + using Distance = float; + + glm::vec<3, Distance> normal; + Distance distance; Plane() - : distance(0.0f) + : distance(0.0) { } - Plane(const glm::vec3& normal, float distance) + Plane(const glm::vec<3, Distance>& normal, Distance distance) : normal(normal), distance(distance) { } @@ -81,87 +85,13 @@ std::vector DebugColorFacesByPlane(const std::vector& verti // Build all convex hulls for regions in areas std::vector BuildConvexHullsFromRegions(const eqg::Terrain& terrain); -// Result structure for a single area's BSP tree -struct AreaBSPTree -{ - // BSP node for area-specific trees - struct Node - { - glm::vec3 normal; - float dist = 0.f; - uint32_t region; // 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) - }; - - uint32_t areaNum; - uint32_t rootNum; - std::unordered_map nodes; -}; - -struct BRepVolume -{ - using Vec3 = glm::vec<3, double>; - using Vec2 = glm::vec<2, double>; - - struct Face - { - std::vector vertexes; - int planeId; - int areaId = 0; - bool valid = true; - - Face() = default; - Face(std::vector v, int pid, int a = 0) - : vertexes(std::move(v)), planeId(pid), areaId(a) {} - }; - - struct Segment - { - Vec2 start; - Vec2 end; - - [[nodiscard]] Vec2 midpoint() const { return (start + end) * 0.5; } - [[nodiscard]] bool isCoincident(const std::vector& boundary) const; - [[nodiscard]] bool isCollinearAndOverlapping(const Segment& other) const; - }; - - struct ClassifiedSegment : Segment - { - enum class Classification - { - INSIDE, - OUTSIDE, - COINCIDENT - }; - - Classification classification; - }; - - struct PlaneBasis - { - Vec3 origin; // point on the plane - Vec3 u; // first basis vector (tangent) - Vec3 v; // second basis vector (bitangent) - - [[nodiscard]] Vec2 project(const Vec3& point) const; - [[nodiscard]] Vec3 unproject(const Vec2& point) const; - }; - - std::vector faces; -}; - // Result of building a BRep from BSP planes struct BRepResult { - int areaIndex; + int areaIndex = -1; std::vector vertexes; std::vector> faces; // Polygon faces (not triangulated) }; -// 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 BuildBRepsFromAreas(const eqg::Terrain& terrain); - +std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain); diff --git a/meshgen/MeshGenerator.vcxproj b/meshgen/MeshGenerator.vcxproj index ad0e8494..f4966e21 100644 --- a/meshgen/MeshGenerator.vcxproj +++ b/meshgen/MeshGenerator.vcxproj @@ -16,6 +16,7 @@ + @@ -263,7 +264,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) + triangle.lib;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 +292,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) + triangle.lib;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/MeshGenerator.vcxproj.filters b/meshgen/MeshGenerator.vcxproj.filters index 5a3e9617..11fe6bfc 100644 --- a/meshgen/MeshGenerator.vcxproj.filters +++ b/meshgen/MeshGenerator.vcxproj.filters @@ -197,6 +197,9 @@ Source Files + + Source Files + Source Files\engine diff --git a/meshgen/ZoneResourceManager.cpp b/meshgen/ZoneResourceManager.cpp index 4e372f56..d9396360 100644 --- a/meshgen/ZoneResourceManager.cpp +++ b/meshgen/ZoneResourceManager.cpp @@ -1332,7 +1332,7 @@ void ZoneResourceManager::CreateWldAreaEntities2(const eqg::Terrain& terrain) SPDLOG_INFO("Generating area volumes from BSP Tree"); - auto breps = BuildBRepsFromAreas(terrain); + auto breps = convertBSPToBRepPolyhedraUnion(terrain); std::unordered_map areaVolumeComponents; std::unordered_map handles; 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 From 2ad22323ff3c9d297ef18c13f16b587b66aba473 Mon Sep 17 00:00:00 2001 From: dannuic Date: Fri, 13 Feb 2026 17:24:52 -0700 Subject: [PATCH 4/8] Working solution --- meshgen/AreaVolumeRenderSystem.cpp | 74 ++++---- meshgen/BRepConverter.cpp | 152 ++++++++-------- meshgen/EQComponents.h | 3 +- meshgen/GeometryUtils.cpp | 4 +- meshgen/GeometryUtils.h | 7 +- meshgen/ZoneResourceManager.cpp | 280 ++++++++++++++--------------- meshgen/ZoneResourceManager.h | 3 +- 7 files changed, 259 insertions(+), 264 deletions(-) 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/BRepConverter.cpp b/meshgen/BRepConverter.cpp index 37d84b5c..95d9e49c 100644 --- a/meshgen/BRepConverter.cpp +++ b/meshgen/BRepConverter.cpp @@ -86,7 +86,6 @@ struct AreaBSPTree struct Vertex { Vec3 position; - int halfEdge = -1; // one of the outgoing half edges for traversal int id = -1; Vertex() = default; @@ -94,17 +93,23 @@ struct Vertex : 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 vertexes; std::vector vertexIds; Plane plane; int id = -1; Face() = default; - explicit Face(std::vector vertexes, const Plane& plane, int id) - : vertexes(std::move(vertexes)), plane(plane), id(id) {} - explicit Face(const Plane& plane, int id) : plane(plane), id(id) {} }; @@ -114,6 +119,7 @@ struct BRep std::vector vertexes; std::vector faces; + std::vector outerEdges; int addVertex(const Vec3& position) { @@ -388,6 +394,7 @@ Clipper2Lib::PathsD triangulate(const Clipper2Lib::PathsD& paths) { Clipper2Lib::PathsD faces; + // TODO: try to use these edges to eliminate the extra edges in the result if (!paths.empty()) { CDT::Triangulation cdt( @@ -435,6 +442,20 @@ Clipper2Lib::PathsD triangulate(const Clipper2Lib::PathsD& paths) BRep groupFaces(const std::vector& allFaces) { + BRep result; + constexpr Distance VERTEX_MERGE_TOL = 1e-2f; + std::vector vertexPositions; + + auto findOrAddVertex = [&result, &vertexPositions](const Vec3& pos) -> int + { + 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); + }; + // Group faces by coplanar plane (rounded normal + distance) auto planeKey = [](const Plane& p) { return std::make_tuple( @@ -469,7 +490,6 @@ BRep groupFaces(const std::vector& allFaces) } } - std::map, std::vector> xoredFaces; for (const auto& [key, faces] : groups) { PlaneBasis basis = PlaneBasis::fromPlane(faces[0].plane); @@ -513,27 +533,32 @@ BRep groupFaces(const std::vector& allFaces) auto diffed = Clipper2Lib::Difference(paths, inversePaths, Clipper2Lib::FillRule::NonZero); diffed = Clipper2Lib::Union(diffed, Clipper2Lib::FillRule::NonZero); - diffed = Clipper2Lib::SimplifyPaths(diffed, 1); - Clipper2Lib::PathsD solution = triangulate(diffed); - //Clipper2Lib::Triangulate(diffed, 0, solution); - solution = Clipper2Lib::SimplifyPaths(solution, 1); - if (!solution.empty()) + for (const auto& path : solution) { - xoredFaces[key].reserve(solution.size()); - for (const auto& path : solution) + if (!path.empty()) { - if (!path.empty()) - { - MergeFace newFace; - newFace.plane = faces[0].plane; - newFace.vertexes.reserve(path.size()); - for (const auto& point : path) - newFace.vertexes.push_back(basis.unproject({point.x, point.y})); + std::vector vertexIds; + vertexIds.reserve(path.size()); + for (const auto& point : path) + vertexIds.push_back(findOrAddVertex(basis.unproject({point.x, point.y}))); - xoredFaces[key].push_back(newFace); - } + result.faces[result.addFace(faces[0].plane)].vertexIds = std::move(vertexIds); + } + } + + for (const auto& path : diffed) + { + if (!path.empty()) + { + std::vector vertexIds; + vertexIds.reserve(path.size()); + for (const auto& point : path) + vertexIds.push_back(findOrAddVertex(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()]); } } } @@ -556,56 +581,33 @@ BRep groupFaces(const std::vector& allFaces) } auto unioned = Clipper2Lib::Union(paths, Clipper2Lib::FillRule::NonZero); - unioned = Clipper2Lib::SimplifyPaths(unioned, 1); - Clipper2Lib::PathsD solution = triangulate(unioned); - // Clipper2Lib::Triangulate(unioned, 0, solution); - solution = Clipper2Lib::SimplifyPaths(solution, 1); - if (!solution.empty()) + for (const auto& path : solution) { - xoredFaces[key].reserve(solution.size()); - for (const auto& path : solution) + if (!path.empty()) { - if (!path.empty()) - { - MergeFace newFace; - newFace.plane = faces[0].plane; - newFace.vertexes.reserve(path.size()); - for (const auto& point : path) - newFace.vertexes.push_back(basis.unproject({point.x, point.y})); + std::vector vertexIds; + for (const auto& point : path) + vertexIds.push_back(findOrAddVertex(basis.unproject({point.x, point.y}))); - xoredFaces[key].push_back(newFace); - } + result.faces[result.addFace(faces[0].plane)].vertexIds = std::move(vertexIds); } } - } - } - - BRep result; - constexpr Distance VERTEX_MERGE_TOL = 1e-2f; - std::vector vertexPositions; - - auto findOrAddVertex = [&result, &vertexPositions](const Vec3& pos) -> int - { - 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); - }; - for (const auto& [key, faces] : xoredFaces) - { - for (const auto& face : faces) - { - std::vector vertexIds; - for (const auto& vertex : face.vertexes) - vertexIds.push_back(findOrAddVertex(vertex)); + for (const auto& path : unioned) + { + if (!path.empty()) + { + std::vector vertexIds; + vertexIds.reserve(path.size()); + for (const auto& point : path) + vertexIds.push_back(findOrAddVertex(basis.unproject({point.x, point.y}))); - int faceId = result.addFace(face.plane); - result.faces[faceId].vertexIds = std::move(vertexIds); + for (size_t i = 0; i < vertexIds.size(); ++i) + result.outerEdges.emplace_back(vertexIds[i], vertexIds[(i + 1) % vertexIds.size()]); + } + } } } @@ -617,15 +619,10 @@ BRep groupFaces(const std::vector& allFaces) #pragma region Polyhedra Union -class PolyhedraUnionConverter -{ -public: - static BRepResult convert(const std::vector& hulls); -}; // --- Main conversion --- -BRepResult PolyhedraUnionConverter::convert(const std::vector& hulls) +BRepResult convert(const std::vector& hulls) { BRepResult result; @@ -684,8 +681,17 @@ BRepResult PolyhedraUnionConverter::convert(const std::vector& [](const Vertex& vert) { return vert.position; }); for (const auto& face : volume.faces) - if (face.vertexIds.size() >= 3) - result.faces.emplace_back(face.vertexIds.begin(), face.vertexIds.end()); + 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) { @@ -716,8 +722,8 @@ std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terra for (const auto& [area, convexHulls] : convexHulls) { - auto result = PolyhedraUnionConverter::convert(convexHulls); - result.areaIndex = area; + auto result = convert(convexHulls); + result.areaIndex = static_cast(area); saveOBJ(result, fmt::format("test_{}.obj", area)); results.push_back(std::move(result)); 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 fb1f0616..177c0301 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -283,7 +283,7 @@ struct PlaneHasher }; // Compute plane from a polygon face using Newell's method -Plane ComputeFacePlane(const std::vector& vertices, const std::vector& face) +Plane ComputeFacePlane(const std::vector& vertices, const std::array& face) { Plane plane{ glm::vec3(0.0f), 0.0f }; @@ -335,7 +335,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 8560de0a..16d5de2a 100644 --- a/meshgen/GeometryUtils.h +++ b/meshgen/GeometryUtils.h @@ -13,8 +13,6 @@ #include "eqglib/eqg_terrain.h" // Forward declarations -struct AreaVolumeComponent; - namespace eqg { class Terrain; @@ -80,7 +78,7 @@ 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); @@ -91,7 +89,8 @@ struct BRepResult int areaIndex = -1; std::vector vertexes; - std::vector> faces; // Polygon faces (not triangulated) + std::vector> faces; // Polygon faces (triangulated) + std::vector> outerEdges; }; std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain); diff --git a/meshgen/ZoneResourceManager.cpp b/meshgen/ZoneResourceManager.cpp index d9396360..4b990d60 100644 --- a/meshgen/ZoneResourceManager.cpp +++ b/meshgen/ZoneResourceManager.cpp @@ -191,7 +191,7 @@ bool ZoneResourceManager::BuildScene(Scene& scene) // WLD zones use BSP trees for area bounds if (terrain->m_wldBspTree && !terrain->m_wldAreas.empty()) { - CreateWldAreaEntities2(*terrain); + CreateWldAreaEntities(*terrain); } } @@ -1203,127 +1203,127 @@ void ZoneResourceManager::AddArea(const eqg::TerrainAreaPtr& areaPtr) renderComponent.color = AreaEnvironmentToColor(areaPtr->environment); } -void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) -{ - if (!terrain.m_wldBspTree) - { - return; - } - - SPDLOG_INFO("Generating area volumes from BSP Tree"); - - auto hulls = BuildConvexHullsFromRegions(terrain); - - std::unordered_map areaVolumeComponents; - std::unordered_map handles; - - auto& areaIndices = terrain.m_wldAreaIndices; - auto& areas = terrain.m_wldAreas; - auto& environments = terrain.m_wldAreaEnvironments; - - for (auto& hull : hulls) - { - if (hull.vertices.empty() || hull.faces.empty()) - continue; - - 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); - - 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(); - } - - uint16_t baseIndex = static_cast(areaVolumeComponent->vertices.size()); - - // 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) - { - adjustedFace.push_back(baseIndex + idx); - } - areaVolumeComponent->faces.push_back(std::move(adjustedFace)); - } - - // Store triangulated hull for navmesh use - convexHull->vertices = hull.vertices; - - for (const auto& face : hull.faces) - { - // Fan triangulation from first vertex - for (size_t i = 1; i + 1 < face.size(); ++i) - { - convexHull->indices.push_back(face[0]); - convexHull->indices.push_back(face[i]); - convexHull->indices.push_back(face[i + 1]); - } - } - } - - // Now that we have all the convex hulls consolidated, process each area - for (const auto& [areaIndex, areaVolume] : areaVolumeComponents) - { - // Create a transform to give this object local space coordinates. - glm::vec3 center{ 0.0f, 0.0f, 0.0f }; - - // Calculate the center - for (const glm::vec3& vertex : areaVolume->vertices) - { - center += vertex; - } - center /= static_cast(areaVolume->vertices.size()); - - // Center the volume on the new center point - for (glm::vec3& vertex : areaVolume->vertices) - { - vertex -= center; - } - - TransformComponent& transform = handles[areaIndex].get(); - transform.position = center; - - // Add render component with fill color from WldAreaComponent - WldAreaComponent& wldArea = handles[areaIndex].get(); - AreaVolumeRenderComponent& renderComp = handles[areaIndex].emplace(); - - mq::MQColor fillColor = wldArea.color; - fillColor.Alpha = 51; // 20% opacity - renderComp.color = fillColor.ToABGR(); - } - - SPDLOG_INFO("Created {} WLD area entities from BSP tree", areaVolumeComponents.size()); -} +// void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) +// { +// if (!terrain.m_wldBspTree) +// { +// return; +// } +// +// SPDLOG_INFO("Generating area volumes from BSP Tree"); +// +// auto hulls = BuildConvexHullsFromRegions(terrain); +// +// std::unordered_map areaVolumeComponents; +// std::unordered_map handles; +// +// auto& areaIndices = terrain.m_wldAreaIndices; +// auto& areas = terrain.m_wldAreas; +// auto& environments = terrain.m_wldAreaEnvironments; +// +// for (auto& hull : hulls) +// { +// if (hull.vertices.empty() || hull.faces.empty()) +// continue; +// +// 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); +// +// 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(); +// } +// +// uint16_t baseIndex = static_cast(areaVolumeComponent->vertices.size()); +// +// // 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) +// { +// adjustedFace.push_back(baseIndex + idx); +// } +// areaVolumeComponent->faces.push_back(std::move(adjustedFace)); +// } +// +// // Store triangulated hull for navmesh use +// convexHull->vertices = hull.vertices; +// +// for (const auto& face : hull.faces) +// { +// // Fan triangulation from first vertex +// for (size_t i = 1; i + 1 < face.size(); ++i) +// { +// convexHull->indices.push_back(face[0]); +// convexHull->indices.push_back(face[i]); +// convexHull->indices.push_back(face[i + 1]); +// } +// } +// } +// +// // Now that we have all the convex hulls consolidated, process each area +// for (const auto& [areaIndex, areaVolume] : areaVolumeComponents) +// { +// // Create a transform to give this object local space coordinates. +// glm::vec3 center{ 0.0f, 0.0f, 0.0f }; +// +// // Calculate the center +// for (const glm::vec3& vertex : areaVolume->vertices) +// { +// center += vertex; +// } +// center /= static_cast(areaVolume->vertices.size()); +// +// // Center the volume on the new center point +// for (glm::vec3& vertex : areaVolume->vertices) +// { +// vertex -= center; +// } +// +// TransformComponent& transform = handles[areaIndex].get(); +// transform.position = center; +// +// // Add render component with fill color from WldAreaComponent +// WldAreaComponent& wldArea = handles[areaIndex].get(); +// AreaVolumeRenderComponent& renderComp = handles[areaIndex].emplace(); +// +// mq::MQColor fillColor = wldArea.color; +// fillColor.Alpha = 51; // 20% opacity +// renderComp.color = fillColor.ToABGR(); +// } +// +// SPDLOG_INFO("Created {} WLD area entities from BSP tree", areaVolumeComponents.size()); +// } -void ZoneResourceManager::CreateWldAreaEntities2(const eqg::Terrain& terrain) +void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) const { if (!terrain.m_wldBspTree) { @@ -1342,11 +1342,10 @@ void ZoneResourceManager::CreateWldAreaEntities2(const eqg::Terrain& terrain) for (auto& brep : breps) { - if (!brep.vertexes.empty() && !brep.faces.empty()) + if (!brep.vertexes.empty() && !brep.faces.empty()) // TODO: are we okay with no edges? { uint32_t areaIndex = brep.areaIndex; AreaVolumeComponent* areaVolumeComponent; - ConvexHullComponent* convexHull = nullptr; // TODO: this isn't necessarily convex, but reuse the struct auto iter = areaVolumeComponents.find(areaIndex); if (iter == areaVolumeComponents.end()) @@ -1366,44 +1365,35 @@ void ZoneResourceManager::CreateWldAreaEntities2(const eqg::Terrain& terrain) areaVolumeComponent = &entity.emplace(); areaVolumeComponents.emplace(areaIndex, areaVolumeComponent); - convexHull = &wldComponent->hulls.emplace_back(); } else { areaVolumeComponent = iter->second; - convexHull = &handles[areaIndex].get().hulls.emplace_back(); } - uint16_t baseIndex = static_cast(areaVolumeComponent->vertices.size()); + auto baseIndex = static_cast(areaVolumeComponent->vertices.size()); - // Combine vertices & face indices into AreaVolumeComponent + // 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::vector adjustedFace; - adjustedFace.reserve(face.size()); - for (uint16_t idx : face) - { - adjustedFace.push_back(baseIndex + idx); - } - areaVolumeComponent->faces.push_back(std::move(adjustedFace)); + std::array adjustedFace{}; + adjustedFace[0] = face[0] + baseIndex; + adjustedFace[1] = face[1] + baseIndex; + adjustedFace[2] = face[2] + baseIndex; + areaVolumeComponent->faces.push_back(adjustedFace); } - // Store triangulated hull for navmesh use - convexHull->vertices = brep.vertexes; - - for (const auto& face : brep.faces) + areaVolumeComponent->outerEdges.reserve(areaVolumeComponent->outerEdges.size() + brep.outerEdges.size()); + for (const auto& outerEdge : brep.outerEdges) { - // Fan triangulation from first vertex - for (size_t i = 1; i + 1 < face.size(); ++i) - { - 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 96ffc325..3dd33360 100644 --- a/meshgen/ZoneResourceManager.h +++ b/meshgen/ZoneResourceManager.h @@ -94,8 +94,7 @@ class ZoneResourceManager void RemovePointLight(const eqg::PointLightPtr& light); void AddArea(const eqg::TerrainAreaPtr& areaPtr); - void CreateWldAreaEntities(const eqg::Terrain& terrain); - void CreateWldAreaEntities2(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); From c147aa19e209e491b4a13467164a21a540706f7d Mon Sep 17 00:00:00 2001 From: dannuic Date: Fri, 13 Feb 2026 23:41:34 -0700 Subject: [PATCH 5/8] Fixed most extra edges, consolidated geometry into a single file --- meshgen/BRepConverter.cpp | 737 -------------------------- meshgen/GeometryUtils.cpp | 644 +++++++++++++++++++++- meshgen/GeometryUtils.h | 8 +- meshgen/MeshGenerator.vcxproj | 1 - meshgen/MeshGenerator.vcxproj.filters | 3 - 5 files changed, 644 insertions(+), 749 deletions(-) delete mode 100644 meshgen/BRepConverter.cpp diff --git a/meshgen/BRepConverter.cpp b/meshgen/BRepConverter.cpp deleted file mode 100644 index 95d9e49c..00000000 --- a/meshgen/BRepConverter.cpp +++ /dev/null @@ -1,737 +0,0 @@ -// -// Created by dannu on 2/9/2026. -// - -#include "meshgen/GeometryUtils.h" - -#include "eqglib/eqg_terrain.h" -#include "spdlog/spdlog.h" -#include -#include -#include "triangle.h" -#include "CDT.h" - -#include -#include -#include -#include -#include -#include -#include - -using Distance = float; -using Vec3 = glm::vec<3, Distance>; -using Vec2 = glm::vec<2, Distance>; - -constexpr Distance EPSILON = glm::epsilon(); -constexpr Distance PLANE_THICKNESS = static_cast(1e-6); - -// Orthonormal basis for projecting 3D points onto a plane -struct PlaneBasis -{ - Vec3 origin; // A point on the plane - Vec3 u; // First basis vector (tangent) - 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 - Vec3 up = glm::abs(plane.normal.y) < 0.9 ? Vec3(0, 1, 0) : 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]] Vec2 project(const Vec3& point) const - { - Vec3 relative = point - origin; - return {glm::dot(relative, u), glm::dot(relative, v)}; - } - - // Unproject 2D coordinates back to 3D point on the plane - [[nodiscard]] Vec3 unproject(const Vec2& point) const - { - return origin + point.x * u + point.y * v; - } -}; - -// BSP node for area-specific trees -struct Node -{ - Vec3 normal; - Distance 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 -{ - Vec3 position; - int id = -1; - - Vertex() = default; - explicit Vertex(const 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 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; - } -}; - - -#pragma region BSP Debugging Functions - - -// 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 Volume Simplification - - -struct MergeFace -{ - std::vector vertexes; // CCW order when viewed from normal direction - Plane plane; - bool valid = true; - - MergeFace() = default; - MergeFace(std::vector verts, const Plane& p) - : vertexes(std::move(verts)), plane(p) {} -}; - -struct Segment -{ - Vec3 start; - Vec3 end; -}; - -Clipper2Lib::PathsD triangulate(const Clipper2Lib::PathsD& paths) -{ - Clipper2Lib::PathsD faces; - - // TODO: try to use these edges to eliminate the extra edges in the result - 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; -} - -BRep groupFaces(const std::vector& allFaces) -{ - BRep result; - constexpr Distance VERTEX_MERGE_TOL = 1e-2f; - std::vector vertexPositions; - - auto findOrAddVertex = [&result, &vertexPositions](const Vec3& pos) -> int - { - 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); - }; - - // 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) - if (face.valid) - groups[planeKey(face.plane)].push_back(face); - - std::map, std::vector> test; - for (const auto& [key, faces] : groups) - { - PlaneBasis basis = PlaneBasis::fromPlane(faces[0].plane); - test[key].reserve(faces.size()); - for (const auto& face : faces) - { - std::vector vertexes; - vertexes.reserve(face.vertexes.size()); - for (const auto& vertex : face.vertexes) - { - auto projected = basis.project(vertex); - auto unprojected = basis.unproject(projected); - vertexes.emplace_back(unprojected); - } - - test[key].emplace_back(vertexes, face.plane); - } - } - - 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}; // TODO: might need to check +/- 1 for all of these - if (groups.contains(inverse)) // can assume non-empty here - { - auto inverseFaces = groups[inverse]; - Clipper2Lib::PathsD inversePaths; - inversePaths.reserve(inverseFaces.size()); - // the orientation of the face does not appear to matter to the Clipper2 boolean operations - for (const auto& face : inverseFaces) - { - std::vector inversePoints; - inversePoints.reserve(face.vertexes.size() * 2); - for (const auto& vertex : face.vertexes) - { - auto projected = basis.project(vertex); - inversePoints.emplace_back(projected.x); - inversePoints.emplace_back(projected.y); - } - - inversePaths.emplace_back(Clipper2Lib::MakePathD(inversePoints)); - } - - 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)); - } - - auto diffed = Clipper2Lib::Difference(paths, inversePaths, Clipper2Lib::FillRule::NonZero); - diffed = Clipper2Lib::Union(diffed, Clipper2Lib::FillRule::NonZero); - Clipper2Lib::PathsD solution = triangulate(diffed); - - for (const auto& path : solution) - { - if (!path.empty()) - { - std::vector vertexIds; - vertexIds.reserve(path.size()); - for (const auto& point : path) - vertexIds.push_back(findOrAddVertex(basis.unproject({point.x, point.y}))); - - result.faces[result.addFace(faces[0].plane)].vertexIds = std::move(vertexIds); - } - } - - for (const auto& path : diffed) - { - if (!path.empty()) - { - std::vector vertexIds; - vertexIds.reserve(path.size()); - for (const auto& point : path) - vertexIds.push_back(findOrAddVertex(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()]); - } - } - } - else if (!faces.empty()) - { - 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)); - } - - auto unioned = Clipper2Lib::Union(paths, Clipper2Lib::FillRule::NonZero); - Clipper2Lib::PathsD solution = triangulate(unioned); - - for (const auto& path : solution) - { - if (!path.empty()) - { - std::vector vertexIds; - for (const auto& point : path) - vertexIds.push_back(findOrAddVertex(basis.unproject({point.x, point.y}))); - - result.faces[result.addFace(faces[0].plane)].vertexIds = std::move(vertexIds); - } - } - - for (const auto& path : unioned) - { - if (!path.empty()) - { - std::vector vertexIds; - vertexIds.reserve(path.size()); - for (const auto& point : path) - vertexIds.push_back(findOrAddVertex(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()]); - } - } - } - } - - return result; -} - -#pragma endregion - - -#pragma region Polyhedra Union - - -// --- Main conversion --- - -BRepResult convert(const std::vector& hulls) -{ - BRepResult result; - - std::vector allCellFaces; - for (const auto& hull : hulls) - { - std::vector hullVertexes; - hullVertexes.reserve(hull.vertices.size()); - for (const auto& vertex : hull.vertices) - hullVertexes.emplace_back(vertex.x, vertex.y, vertex.z); - - for (const auto& face : hull.faces) - { - if (face.size() >= 3) - { - std::vector vertexes; - vertexes.reserve(face.size()); - for (const auto& vertexIdx : face) - vertexes.push_back(hullVertexes[vertexIdx]); - - // Use Newell's method to compute robust normal for arbitrary polygon - Vec3 normal; - Vec3 centroid; - for (size_t i = 0; i < vertexes.size(); ++i) - { - const Vec3& current = vertexes[i]; - const Vec3& next = vertexes[(i + 1) % vertexes.size()]; - - normal.x += (current.y - next.y) * (current.z + next.z); - normal.y += (current.z - next.z) * (current.x + next.x); - normal.z += (current.x - next.x) * (current.y + next.y); - - centroid += current; - } - - Distance len = glm::length(normal); - if (len > EPSILON) - { - Plane plane; - plane.normal = normal / len; - centroid /= static_cast(vertexes.size()); - plane.distance = glm::dot(plane.normal, centroid); - - allCellFaces.emplace_back(vertexes, plane); - } - } - } - } - - try - { - BRep volume = groupFaces(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(e.what()); - } - - return result; -} - -std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain) -{ - std::vector results; - - if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) - return results; - - auto hulls = BuildConvexHullsFromRegions(terrain); - std::map> convexHulls; - 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); - convexHulls[it->second].emplace_back(hull); - } - - for (const auto& [area, convexHulls] : convexHulls) - { - auto result = convert(convexHulls); - result.areaIndex = static_cast(area); - saveOBJ(result, fmt::format("test_{}.obj", area)); - - results.push_back(std::move(result)); - } - - SPDLOG_INFO("Built {} BReps from WLD BSP tree", results.size()); - return results; -} - - -#pragma endregion diff --git a/meshgen/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index 177c0301..d4b38e77 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -7,6 +7,8 @@ #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" @@ -17,12 +19,126 @@ #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; + } +}; + + +#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) @@ -236,6 +352,527 @@ 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 +//============================================================================================================ + +struct MergeFace +{ + std::vector vertexes; // CCW order when viewed from normal direction + Plane plane; + bool valid = true; + + MergeFace() = default; + MergeFace(std::vector verts, const Plane& p) + : vertexes(std::move(verts)), plane(p) {} +}; + +struct Segment +{ + glm::vec3 start; + glm::vec3 end; +}; + +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 groupFaces(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) + if (face.valid) + 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 = groupFaces(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 convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain) +{ + std::vector results; + + if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) + return results; + + auto hulls = BuildConvexHullsFromRegions(terrain); + std::map> convexHulls; + 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); + convexHulls[it->second].emplace_back(hull); + } + + for (const auto& [area, convexHulls] : convexHulls) + { + auto result = convert(convexHulls); + result.areaIndex = static_cast(area); + saveOBJ(result, fmt::format("test_{}.obj", area)); + + results.push_back(std::move(result)); + } + + SPDLOG_INFO("Built {} BReps from WLD BSP tree", results.size()); + return results; +} + +//============================================================================================================ +//============================================================================================================ + template <> struct std::hash { @@ -283,7 +920,8 @@ struct PlaneHasher }; // Compute plane from a polygon face using Newell's method -Plane ComputeFacePlane(const std::vector& vertices, const std::array& face) +template +Plane ComputeFacePlane(const std::vector& vertices, const T& face) { Plane plane{ glm::vec3(0.0f), 0.0f }; diff --git a/meshgen/GeometryUtils.h b/meshgen/GeometryUtils.h index 16d5de2a..6ca4acf8 100644 --- a/meshgen/GeometryUtils.h +++ b/meshgen/GeometryUtils.h @@ -30,17 +30,15 @@ struct ConvexHullResult // Simple plane representation struct Plane { - using Distance = float; - - glm::vec<3, Distance> normal; - Distance distance; + glm::vec3 normal; + float distance; Plane() : distance(0.0) { } - Plane(const glm::vec<3, Distance>& normal, Distance distance) + Plane(const glm::vec3& normal, float distance) : normal(normal), distance(distance) { } diff --git a/meshgen/MeshGenerator.vcxproj b/meshgen/MeshGenerator.vcxproj index f4966e21..858698b4 100644 --- a/meshgen/MeshGenerator.vcxproj +++ b/meshgen/MeshGenerator.vcxproj @@ -16,7 +16,6 @@ - diff --git a/meshgen/MeshGenerator.vcxproj.filters b/meshgen/MeshGenerator.vcxproj.filters index 11fe6bfc..5a3e9617 100644 --- a/meshgen/MeshGenerator.vcxproj.filters +++ b/meshgen/MeshGenerator.vcxproj.filters @@ -197,9 +197,6 @@ Source Files - - Source Files - Source Files\engine From 28fdea14c2c7f7e215094777b8ab5d420f128f7a Mon Sep 17 00:00:00 2001 From: dannuic Date: Fri, 13 Feb 2026 23:54:00 -0700 Subject: [PATCH 6/8] Added hulls output to be used later in ZoneResourceManager --- meshgen/GeometryUtils.cpp | 83 +++++++++++---------- meshgen/GeometryUtils.h | 2 +- meshgen/ZoneResourceManager.cpp | 123 +------------------------------- 3 files changed, 43 insertions(+), 165 deletions(-) diff --git a/meshgen/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index d4b38e77..26c086dc 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -132,6 +132,22 @@ struct BRep } }; +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 @@ -596,24 +612,7 @@ std::vector BuildAreaBSPTrees(const eqg::Terrain& terrain) // Polygon Simplification for Convex Hulls -> Triangulated BRep //============================================================================================================ -struct MergeFace -{ - std::vector vertexes; // CCW order when viewed from normal direction - Plane plane; - bool valid = true; - - MergeFace() = default; - MergeFace(std::vector verts, const Plane& p) - : vertexes(std::move(verts)), plane(p) {} -}; - -struct Segment -{ - glm::vec3 start; - glm::vec3 end; -}; - -Clipper2Lib::PathsD triangulate(const Clipper2Lib::PathsD& paths) +Clipper2Lib::PathsD Triangulate(const Clipper2Lib::PathsD& paths) { Clipper2Lib::PathsD faces; @@ -662,7 +661,7 @@ Clipper2Lib::PathsD triangulate(const Clipper2Lib::PathsD& paths) return faces; } -Clipper2Lib::PathsD extractPaths(const std::vector& faces, const PlaneBasis& basis) +Clipper2Lib::PathsD ExtractPaths(const std::vector& faces, const PlaneBasis& basis) { Clipper2Lib::PathsD paths; paths.reserve(faces.size()); @@ -683,7 +682,7 @@ Clipper2Lib::PathsD extractPaths(const std::vector& faces, const Plan return paths; } -int findOrAddVertex(BRep& result, std::vector& vertexPositions, const glm::vec3& pos) +int FindOrAddVertex(BRep& result, std::vector& vertexPositions, const glm::vec3& pos) { constexpr float VERTEX_MERGE_TOL = 1e-2f; @@ -695,12 +694,12 @@ int findOrAddVertex(BRep& result, std::vector& vertexPositions, const return static_cast(vertexPositions.size() - 1); } -void addFacesAndEdges(BRep& result, std::vector& vertexPositions, const Clipper2Lib::PathsD& paths, const Plane& plane, const PlaneBasis& basis) +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); + auto triangulated = Triangulate(simplified); for (const auto& path : triangulated) { @@ -709,7 +708,7 @@ void addFacesAndEdges(BRep& result, std::vector& vertexPositions, con std::vector vertexIds; vertexIds.reserve(path.size()); for (const auto& point : path) - vertexIds.push_back(findOrAddVertex(result, vertexPositions, basis.unproject({point.x, point.y}))); + vertexIds.push_back(FindOrAddVertex(result, vertexPositions, basis.unproject({point.x, point.y}))); result.faces[result.addFace(plane)].vertexIds = std::move(vertexIds); } @@ -723,7 +722,7 @@ void addFacesAndEdges(BRep& result, std::vector& vertexPositions, con std::vector vertexIds; vertexIds.reserve(trimmedPath.size()); for (const auto& point : trimmedPath) - vertexIds.push_back(findOrAddVertex(result, vertexPositions, basis.unproject({point.x, point.y}))); + 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()]); @@ -732,7 +731,7 @@ void addFacesAndEdges(BRep& result, std::vector& vertexPositions, con } } -BRep groupFaces(const std::vector& allFaces) +BRep ConvertFacesByPlane(const std::vector& allFaces) { BRep result; std::vector vertexPositions; @@ -748,8 +747,7 @@ BRep groupFaces(const std::vector& allFaces) std::map, std::vector> groups; for (auto& face : allFaces) - if (face.valid) - groups[planeKey(face.plane)].push_back(face); + groups[planeKey(face.plane)].push_back(face); for (const auto& [key, faces] : groups) { @@ -759,19 +757,19 @@ BRep groupFaces(const std::vector& allFaces) 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);; + 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); + AddFacesAndEdges(result, vertexPositions, diffed, faces[0].plane, basis); } else if (!faces.empty()) { - Clipper2Lib::PathsD paths = extractPaths(faces, basis); + Clipper2Lib::PathsD paths = ExtractPaths(faces, basis); auto unioned = Clipper2Lib::Union(paths, Clipper2Lib::FillRule::NonZero); - addFacesAndEdges(result, vertexPositions, unioned, faces[0].plane, basis); + AddFacesAndEdges(result, vertexPositions, unioned, faces[0].plane, basis); } } @@ -785,7 +783,7 @@ BRep groupFaces(const std::vector& allFaces) //============================================================================================================ template Plane ComputeFacePlane(const std::vector& vertices, const T& face); -BRepResult convert(const std::vector& hulls) +BRepResult Convert(const std::vector& hulls) { BRepResult result; @@ -811,7 +809,7 @@ BRepResult convert(const std::vector& hulls) try { - BRep volume = groupFaces(allCellFaces); + BRep volume = ConvertFacesByPlane(allCellFaces); // convert volume to result std::transform(volume.vertexes.begin(), volume.vertexes.end(), std::back_inserter(result.vertexes), @@ -838,28 +836,27 @@ BRepResult convert(const std::vector& hulls) return result; } -std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain) +std::vector BuildBRepsFromConvexHulls(const std::vector& hulls, const eqg::Terrain& terrain) { std::vector results; + std::map> hullsPerEnv; - if (!terrain.m_wldBspTree || terrain.m_wldBspTree->nodes.empty()) - return results; - - auto hulls = BuildConvexHullsFromRegions(terrain); - std::map> convexHulls; + // 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); - convexHulls[it->second].emplace_back(hull); + hullsPerEnv[it->second].emplace_back(hull); } - for (const auto& [area, convexHulls] : convexHulls) + for (const auto& [area, convexHulls] : hullsPerEnv) { - auto result = convert(convexHulls); + auto result = Convert(convexHulls); result.areaIndex = static_cast(area); saveOBJ(result, fmt::format("test_{}.obj", area)); diff --git a/meshgen/GeometryUtils.h b/meshgen/GeometryUtils.h index 6ca4acf8..7a3157a6 100644 --- a/meshgen/GeometryUtils.h +++ b/meshgen/GeometryUtils.h @@ -91,4 +91,4 @@ struct BRepResult std::vector> outerEdges; }; -std::vector convertBSPToBRepPolyhedraUnion(const eqg::Terrain& terrain); +std::vector BuildBRepsFromConvexHulls(const std::vector& hulls, const eqg::Terrain& terrain); diff --git a/meshgen/ZoneResourceManager.cpp b/meshgen/ZoneResourceManager.cpp index 4b990d60..fc75f246 100644 --- a/meshgen/ZoneResourceManager.cpp +++ b/meshgen/ZoneResourceManager.cpp @@ -1203,126 +1203,6 @@ void ZoneResourceManager::AddArea(const eqg::TerrainAreaPtr& areaPtr) renderComponent.color = AreaEnvironmentToColor(areaPtr->environment); } -// void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) -// { -// if (!terrain.m_wldBspTree) -// { -// return; -// } -// -// SPDLOG_INFO("Generating area volumes from BSP Tree"); -// -// auto hulls = BuildConvexHullsFromRegions(terrain); -// -// std::unordered_map areaVolumeComponents; -// std::unordered_map handles; -// -// auto& areaIndices = terrain.m_wldAreaIndices; -// auto& areas = terrain.m_wldAreas; -// auto& environments = terrain.m_wldAreaEnvironments; -// -// for (auto& hull : hulls) -// { -// if (hull.vertices.empty() || hull.faces.empty()) -// continue; -// -// 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); -// -// 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(); -// } -// -// uint16_t baseIndex = static_cast(areaVolumeComponent->vertices.size()); -// -// // 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) -// { -// adjustedFace.push_back(baseIndex + idx); -// } -// areaVolumeComponent->faces.push_back(std::move(adjustedFace)); -// } -// -// // Store triangulated hull for navmesh use -// convexHull->vertices = hull.vertices; -// -// for (const auto& face : hull.faces) -// { -// // Fan triangulation from first vertex -// for (size_t i = 1; i + 1 < face.size(); ++i) -// { -// convexHull->indices.push_back(face[0]); -// convexHull->indices.push_back(face[i]); -// convexHull->indices.push_back(face[i + 1]); -// } -// } -// } -// -// // Now that we have all the convex hulls consolidated, process each area -// for (const auto& [areaIndex, areaVolume] : areaVolumeComponents) -// { -// // Create a transform to give this object local space coordinates. -// glm::vec3 center{ 0.0f, 0.0f, 0.0f }; -// -// // Calculate the center -// for (const glm::vec3& vertex : areaVolume->vertices) -// { -// center += vertex; -// } -// center /= static_cast(areaVolume->vertices.size()); -// -// // Center the volume on the new center point -// for (glm::vec3& vertex : areaVolume->vertices) -// { -// vertex -= center; -// } -// -// TransformComponent& transform = handles[areaIndex].get(); -// transform.position = center; -// -// // Add render component with fill color from WldAreaComponent -// WldAreaComponent& wldArea = handles[areaIndex].get(); -// AreaVolumeRenderComponent& renderComp = handles[areaIndex].emplace(); -// -// mq::MQColor fillColor = wldArea.color; -// fillColor.Alpha = 51; // 20% opacity -// renderComp.color = fillColor.ToABGR(); -// } -// -// SPDLOG_INFO("Created {} WLD area entities from BSP tree", areaVolumeComponents.size()); -// } - void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) const { if (!terrain.m_wldBspTree) @@ -1332,7 +1212,8 @@ void ZoneResourceManager::CreateWldAreaEntities(const eqg::Terrain& terrain) con SPDLOG_INFO("Generating area volumes from BSP Tree"); - auto breps = convertBSPToBRepPolyhedraUnion(terrain); + auto hulls = BuildConvexHullsFromRegions(terrain); + auto breps = BuildBRepsFromConvexHulls(hulls, terrain); std::unordered_map areaVolumeComponents; std::unordered_map handles; From 6a7069aec694becd381e963708e1262b66ca6efc Mon Sep 17 00:00:00 2001 From: dannuic Date: Fri, 13 Feb 2026 23:59:23 -0700 Subject: [PATCH 7/8] Removed testing output --- meshgen/GeometryUtils.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/meshgen/GeometryUtils.cpp b/meshgen/GeometryUtils.cpp index 26c086dc..f67bc49c 100644 --- a/meshgen/GeometryUtils.cpp +++ b/meshgen/GeometryUtils.cpp @@ -858,7 +858,6 @@ std::vector BuildBRepsFromConvexHulls(const std::vector(area); - saveOBJ(result, fmt::format("test_{}.obj", area)); results.push_back(std::move(result)); } From 83900e0ab7c4f3f350c5308a43d79d17990fe198 Mon Sep 17 00:00:00 2001 From: dannuic Date: Sat, 14 Feb 2026 00:01:14 -0700 Subject: [PATCH 8/8] Removed unused library --- meshgen/MeshGenerator.vcxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meshgen/MeshGenerator.vcxproj b/meshgen/MeshGenerator.vcxproj index 858698b4..223bfba7 100644 --- a/meshgen/MeshGenerator.vcxproj +++ b/meshgen/MeshGenerator.vcxproj @@ -263,7 +263,7 @@ Windows true - triangle.lib;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) + 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 - triangle.lib;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) + 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