From 25cd598b176318460adc93561153849be7f2abba Mon Sep 17 00:00:00 2001 From: Kawe Mazidjatari <48657826+Mauler125@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:18:51 +0200 Subject: [PATCH] Recast: major improvement to traverse linking algorithm Instead of using just the polygon edges, use the detail edges as well. Ray cast errors have been fully eliminated; nothing ever clips through geometry anymore. The results are also a lot more detailed now as we can connect various sub edges together. The resulting navmesh also performs better in-game, the npc's are now a lot more fluent with jumps and climbs, they rarely clip though geometry, if they do its typically an error with the input geometry used to build the navmesh. There was also a bug in GetBestTraverseType; the exclusive check (where at least one overlap had to occur) returned true if both overlap tests failed. This has been fixed by flipping the check and now checking if at least one overlaps when an exclusive test was specified. --- src/naveditor/Editor.cpp | 504 ++++++++++++++++++++++----------------- 1 file changed, 289 insertions(+), 215 deletions(-) diff --git a/src/naveditor/Editor.cpp b/src/naveditor/Editor.cpp index 36b394b6..6f9d0113 100644 --- a/src/naveditor/Editor.cpp +++ b/src/naveditor/Editor.cpp @@ -692,11 +692,11 @@ TraverseType_e GetBestTraverseType(const float traverseDist, const float elevati if (traverseType.ovlpTrig > -1 && elevation >= traverseType.ovlpTrig) { - const bool noOverlap = traverseType.ovlpExcl - ? (!baseOverlaps && !landOverlaps) - : (!baseOverlaps || !landOverlaps); + const bool overlaps = traverseType.ovlpExcl + ? (baseOverlaps || landOverlaps) + : (baseOverlaps && landOverlaps); - if (noOverlap) + if (!overlaps) continue; } @@ -876,13 +876,18 @@ static bool traverseLinkInLOS(const InputGeom* geom, const float* lowPos, const void Editor::connectTileTraverseLinks(dtMeshTile* const baseTile, const bool linkToNeighbor) { + const dtMeshHeader* baseHeader = baseTile->header; + + if (!baseHeader->detailMeshCount) + return; // Detail meshes are required for traverse links. + // If we link to the same tile, we need at least 2 links. if (!baseTile->linkCountAvailable(linkToNeighbor ? 1 : 2)) return; - const dtMeshHeader* baseHeader = baseTile->header; - const dtPolyRef basePolyRefBase = m_navMesh->getPolyRefBase(baseTile); + static const float detailEdgeAlignThresh = 0.01f*0.01f; + const dtPolyRef basePolyRefBase = m_navMesh->getPolyRefBase(baseTile); bool firstBaseTileLinkUsed = false; for (int i = 0; i < baseHeader->polyCount; ++i) @@ -895,6 +900,8 @@ void Editor::connectTileTraverseLinks(dtMeshTile* const baseTile, const bool lin if (basePoly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) continue; + dtPolyDetail* const baseDetail = &baseTile->detailMeshes[i]; + for (int j = 0; j < basePoly->vertCount; ++j) { // Hard edges only! @@ -902,235 +909,302 @@ void Editor::connectTileTraverseLinks(dtMeshTile* const baseTile, const bool lin continue; // Polygon 1 edge - const float* const basePolySpos = &baseTile->verts[basePoly->verts[j] * 3]; - const float* const basePolyEpos = &baseTile->verts[basePoly->verts[(j + 1) % basePoly->vertCount] * 3]; + const float* const basePolySpos = &baseTile->verts[basePoly->verts[j]*3]; + const float* const basePolyEpos = &baseTile->verts[basePoly->verts[(j+1)%basePoly->vertCount]*3]; - float baseEdgeDir[3]; - rdVsub(baseEdgeDir, basePolyEpos, basePolySpos); - - unsigned char baseSide = rdClassifyDirection(baseEdgeDir, baseHeader->bmin, baseHeader->bmax); - - const int MAX_NEIS = 32; // Max neighbors - dtMeshTile* neis[MAX_NEIS]; - - int nneis = 0; - - if (linkToNeighbor) // Retrieve the neighboring tiles on the side of our base poly edge. + for (int k = 0; k < baseDetail->triCount; ++k) { - nneis = m_navMesh->getNeighbourTilesAt(baseHeader->x, baseHeader->y, baseSide, neis, MAX_NEIS); - - // No neighbors, nothing to link to on this side. - if (!nneis) - continue; - } - else - { - // Internal links. - nneis = 1; - neis[0] = baseTile; - } - - float basePolyEdgeMid[3]; - if (nneis) - rdVsad(basePolyEdgeMid, basePolySpos, basePolyEpos, 0.5f); - - for (int k = nneis-1; k >= 0; --k) - { - dtMeshTile* landTile = neis[k]; - const bool sameTile = baseTile == landTile; - - // Don't connect to same tile edges yet, leave that for the second pass. - if (linkToNeighbor && sameTile) - continue; - - // Skip same polygon. - if (sameTile && i == k) - continue; - - if (!landTile->linkCountAvailable(1)) - continue; - - const dtMeshHeader* landHeader = landTile->header; - const dtPolyRef landPolyRefBase = m_navMesh->getPolyRefBase(landTile); - - bool firstLandTileLinkUsed = false; - - for (int l = 0; l < landHeader->polyCount; ++l) + const unsigned char* baseTri = &baseTile->detailTris[(baseDetail->triBase+k)*4]; + const float* baseTriVerts[3]; + for (int l = 0; l < 3; ++l) { - dtPoly* const landPoly = &landTile->polys[l]; - - if (landPoly->groupId == DT_UNLINKED_POLY_GROUP) + if (baseTri[l] < basePoly->vertCount) + baseTriVerts[l] = &baseTile->verts[basePoly->verts[baseTri[l]]*3]; + else + baseTriVerts[l] = &baseTile->detailVerts[(baseDetail->vertBase+(baseTri[l]-basePoly->vertCount))*3]; + } + for (int l = 0, m = 2; l < 3; m = l++) + { + if ((dtGetDetailTriEdgeFlags(baseTri[3], m) & DT_DETAIL_EDGE_BOUNDARY) == 0) continue; - if (landPoly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) + if (rdDistancePtLine2d(baseTriVerts[m], basePolySpos, basePolyEpos) >= detailEdgeAlignThresh || + rdDistancePtLine2d(baseTriVerts[l], basePolySpos, basePolyEpos) >= detailEdgeAlignThresh) continue; - for (int m = 0; m < landPoly->vertCount; ++m) + const float* baseDetailPolyEdgeSpos = baseTriVerts[m]; + const float* baseDetailPolyEdgeEpos = baseTriVerts[l]; + + float baseTmin; + float baseTmax; + if (!rdCalcSubEdgeArea2D(basePolySpos, basePolyEpos, baseDetailPolyEdgeSpos, baseDetailPolyEdgeEpos, baseTmin, baseTmax)) + continue; + + float baseEdgeDir[3]; + rdVsub(baseEdgeDir, baseDetailPolyEdgeEpos, baseDetailPolyEdgeSpos); + + unsigned char baseSide = rdClassifyDirection(baseEdgeDir, baseHeader->bmin, baseHeader->bmax); + + const int MAX_NEIS = 32; // Max neighbors + dtMeshTile* neis[MAX_NEIS]; + + int nneis = 0; + + if (linkToNeighbor) // Retrieve the neighboring tiles on the side of our base poly edge. { - if (landPoly->neis[m] != 0) + nneis = m_navMesh->getNeighbourTilesAt(baseHeader->x, baseHeader->y, baseSide, neis, MAX_NEIS); + + // No neighbors, nothing to link to on this side. + if (!nneis) + continue; + } + else + { + // Internal links. + nneis = 1; + neis[0] = baseTile; + } + + float basePolyEdgeMid[3]; + if (nneis) + rdVsad(basePolyEdgeMid, baseDetailPolyEdgeSpos, baseDetailPolyEdgeEpos, 0.5f); + + for (int n = nneis - 1; n >= 0; --n) + { + dtMeshTile* landTile = neis[n]; + const bool sameTile = baseTile == landTile; + + // Don't connect to same tile edges yet, leave that for the second pass. + if (linkToNeighbor && sameTile) continue; - // We need at least 2 links available, figure out if - // we link to the same tile or another one. - if (linkToNeighbor) + const dtMeshHeader* landHeader = landTile->header; + + if (!landHeader->detailMeshCount) + continue; // Detail meshes are required for traverse links. + + // Skip same polygon. + if (sameTile && i == n) + continue; + + if (!landTile->linkCountAvailable(1)) + continue; + + const dtPolyRef landPolyRefBase = m_navMesh->getPolyRefBase(landTile); + bool firstLandTileLinkUsed = false; + + for (int o = 0; o < landHeader->polyCount; ++o) { - if (firstLandTileLinkUsed && !landTile->linkCountAvailable(1)) + dtPoly* const landPoly = &landTile->polys[o]; + + if (landPoly->groupId == DT_UNLINKED_POLY_GROUP) continue; - else if (firstBaseTileLinkUsed && !baseTile->linkCountAvailable(1)) - return; - } - else if (firstBaseTileLinkUsed && !baseTile->linkCountAvailable(2)) - return; + if (landPoly->getType() == DT_POLYTYPE_OFFMESH_CONNECTION) + continue; - // Polygon 2 edge - const float* const landPolySpos = &landTile->verts[landPoly->verts[m] * 3]; - const float* const landPolyEpos = &landTile->verts[landPoly->verts[(m + 1) % landPoly->vertCount] * 3]; + dtPolyDetail* const landDetail = &landTile->detailMeshes[o]; - float landPolyEdgeMid[3]; - rdVsad(landPolyEdgeMid, landPolySpos, landPolyEpos, 0.5f); - - const float dist = dtCalcLinkDistance(basePolyEdgeMid, landPolyEdgeMid); - const unsigned char quantDist = dtQuantLinkDistance(dist); - - if (quantDist == 0) - continue; // Link distance is greater than maximum supported. - - float landEdgeDir[3]; - rdVsub(landEdgeDir, landPolyEpos, landPolySpos); - - const float dotProduct = rdVdot(baseEdgeDir, landEdgeDir); - - // Edges facing the same direction should not be linked. - // Doing so causes links to go through from underneath - // geometry. E.g. we have an HVAC on a roof, and we try - // to link our roof poly edge facing north to the edge - // of the poly on the HVAC also facing north, the link - // will go through the HVAC and thus cause the NPC to - // jump through it. - // Another case where this is necessary is when having - // a land edge that connects with the base edge, this - // prevents the algorithm from establishing a parallel - // traverse link. - if (dotProduct > 0) - continue; - - const float elevation = rdMathFabsf(basePolyEdgeMid[2]-landPolyEdgeMid[2]); - const float slopeAngle = rdMathFabsf(rdCalcSlopeAngle(basePolyEdgeMid, landPolyEdgeMid)); - const bool baseOverlaps = calcEdgeOverlap(basePolySpos, basePolyEpos, landPolySpos, landPolyEpos, baseEdgeDir) > m_traverseEdgeMinOverlap; - const bool landOverlaps = calcEdgeOverlap(landPolySpos, landPolyEpos, basePolySpos, basePolyEpos, landEdgeDir) > m_traverseEdgeMinOverlap; - - const TraverseType_e traverseType = GetBestTraverseType(dist, elevation, slopeAngle, baseOverlaps, landOverlaps); - - if (traverseType == DT_NULL_TRAVERSE_TYPE) - continue; - - if (m_selectedNavMeshType == NavMeshType_e::NAVMESH_SMALL) - { - const int tableCount = NavMesh_GetTraverseTableCountForNavMeshType(m_selectedNavMeshType); - bool traverseTypeSupported = false; - - for (int n = 0; n < tableCount; n++) + for (int p = 0; p < landPoly->vertCount; ++p) { - if (rdBitCellBit(traverseType) & s_traverseAnimTraverseFlags[n]) + if (landPoly->neis[p] != 0) + continue; + + // Polygon 2 edge + const float* const landPolySpos = &landTile->verts[landPoly->verts[p]*3]; + const float* const landPolyEpos = &landTile->verts[landPoly->verts[(p+1)%landPoly->vertCount]*3]; + + for (int q = 0; q < landDetail->triCount; ++q) { - traverseTypeSupported = true; - break; + const unsigned char* landTri = &landTile->detailTris[(landDetail->triBase+q)*4]; + const float* landTriVerts[3]; + for (int r = 0; r < 3; ++r) + { + if (landTri[r] < landPoly->vertCount) + landTriVerts[r] = &landTile->verts[landPoly->verts[landTri[r]]*3]; + else + landTriVerts[r] = &landTile->detailVerts[(landDetail->vertBase+(landTri[r]-landPoly->vertCount))*3]; + } + for (int r = 0, s = 2; r < 3; s = r++) + { + // We need at least 2 links available, figure out if + // we link to the same tile or another one. + if (linkToNeighbor) + { + if (firstLandTileLinkUsed && !landTile->linkCountAvailable(1)) + continue; + + else if (firstBaseTileLinkUsed && !baseTile->linkCountAvailable(1)) + return; + } + else if (firstBaseTileLinkUsed && !baseTile->linkCountAvailable(2)) + return; + + if ((dtGetDetailTriEdgeFlags(landTri[3], s) & DT_DETAIL_EDGE_BOUNDARY) == 0) + continue; + + if (rdDistancePtLine2d(landTriVerts[s], landPolySpos, landPolyEpos) >= detailEdgeAlignThresh || + rdDistancePtLine2d(landTriVerts[r], landPolySpos, landPolyEpos) >= detailEdgeAlignThresh) + continue; + + const float* landDetailPolyEdgeSpos = landTriVerts[s]; + const float* landDetailPolyEdgeEpos = landTriVerts[r]; + + float landTmin; + float landTmax; + if (!rdCalcSubEdgeArea2D(landPolySpos, landPolyEpos, landDetailPolyEdgeSpos, landDetailPolyEdgeEpos, landTmin, landTmax)) + continue; + + float landPolyEdgeMid[3]; + rdVsad(landPolyEdgeMid, landDetailPolyEdgeSpos, landDetailPolyEdgeEpos, 0.5f); + + const float dist = dtCalcLinkDistance(basePolyEdgeMid, landPolyEdgeMid); + const unsigned char quantDist = dtQuantLinkDistance(dist); + + if (quantDist == 0) + continue; // Link distance is greater than maximum supported. + + float landEdgeDir[3]; + rdVsub(landEdgeDir, landDetailPolyEdgeEpos, landDetailPolyEdgeSpos); + + const float dotProduct = rdVdot(baseEdgeDir, landEdgeDir); + + // Edges facing the same direction should not be linked. + // Doing so causes links to go through from underneath + // geometry. E.g. we have an HVAC on a roof, and we try + // to link our roof poly edge facing north to the edge + // of the poly on the HVAC also facing north, the link + // will go through the HVAC and thus cause the NPC to + // jump through it. + // Another case where this is necessary is when having + // a land edge that connects with the base edge, this + // prevents the algorithm from establishing a parallel + // traverse link. + if (dotProduct > 0) + continue; + + const float elevation = rdMathFabsf(basePolyEdgeMid[2] - landPolyEdgeMid[2]); + const float slopeAngle = rdMathFabsf(rdCalcSlopeAngle(basePolyEdgeMid, landPolyEdgeMid)); + const bool baseOverlaps = calcEdgeOverlap(baseDetailPolyEdgeSpos, baseDetailPolyEdgeEpos, landDetailPolyEdgeSpos, landDetailPolyEdgeEpos, baseEdgeDir) > m_traverseEdgeMinOverlap; + const bool landOverlaps = calcEdgeOverlap(landDetailPolyEdgeSpos, landDetailPolyEdgeEpos, baseDetailPolyEdgeSpos, baseDetailPolyEdgeEpos, landEdgeDir) > m_traverseEdgeMinOverlap; + + const TraverseType_e traverseType = GetBestTraverseType(dist, elevation, slopeAngle, baseOverlaps, landOverlaps); + + if (traverseType == DT_NULL_TRAVERSE_TYPE) + continue; + + if (m_selectedNavMeshType == NavMeshType_e::NAVMESH_SMALL) + { + const int tableCount = NavMesh_GetTraverseTableCountForNavMeshType(m_selectedNavMeshType); + bool traverseTypeSupported = false; + + for (int t = 0; t < tableCount; t++) + { + if (rdBitCellBit(traverseType) & s_traverseAnimTraverseFlags[t]) + { + traverseTypeSupported = true; + break; + } + } + + if (!traverseTypeSupported) + continue; + } + else + { + const int traverseTableIndex = NavMesh_GetFirstTraverseAnimTypeForType(m_selectedNavMeshType); + const bool traverseTypeSupported = rdBitCellBit(traverseType) & s_traverseAnimTraverseFlags[traverseTableIndex]; + + if (!traverseTypeSupported) + continue; + } + + const dtPolyRef basePolyRef = basePolyRefBase | i; + const dtPolyRef landPolyRef = landPolyRefBase | o; + + const TraverseLinkPolyPair linkedPolyPair(basePolyRef, landPolyRef); + auto linkedIt = m_traverseLinkPolyMap.find(linkedPolyPair); + + bool traverseLinkFound = false; + + if (linkedIt != m_traverseLinkPolyMap.end()) + traverseLinkFound = true; + + // These 2 polygons are already linked with the same traverse type. + if (traverseLinkFound && (rdBitCellBit(traverseType) & linkedIt->second)) + continue; + + const bool basePolyHigher = basePolyEdgeMid[2] > landPolyEdgeMid[2]; + float* const lowerEdgeMid = basePolyHigher ? landPolyEdgeMid : basePolyEdgeMid; + float* const higherEdgeMid = basePolyHigher ? basePolyEdgeMid : landPolyEdgeMid; + float* const lowerEdgeDir = basePolyHigher ? landEdgeDir : baseEdgeDir; + float* const higherEdgeDir = basePolyHigher ? baseEdgeDir : landEdgeDir; + + const float walkableRadius = basePolyHigher ? baseHeader->walkableRadius : landHeader->walkableRadius; + float offsetAmount; + + if (m_traverseRayDynamicOffset) + { + const float totLedgeSpan = walkableRadius + m_traverseRayExtraOffset; + const float maxAngle = rdCalcMaxLOSAngle(totLedgeSpan, m_cellHeight); + + offsetAmount = rdCalcLedgeSpanOffsetAmount(totLedgeSpan, slopeAngle, maxAngle); + } + else + offsetAmount = walkableRadius + m_traverseRayExtraOffset; + + if (!traverseLinkInLOS(m_geom, lowerEdgeMid, higherEdgeMid, lowerEdgeDir, higherEdgeDir, offsetAmount)) + continue; + + const unsigned char landSide = linkToNeighbor + ? rdClassifyPointOutsideBounds(landPolyEdgeMid, landHeader->bmin, landHeader->bmax) + : rdClassifyPointInsideBounds(landPolyEdgeMid, landHeader->bmin, landHeader->bmax); + + const unsigned int forwardIdx = baseTile->allocLink(); + const unsigned int reverseIdx = landTile->allocLink(); + + // Allocated 2 new links, need to check for enough space on subsequent runs. + // This optimization saves a lot of time generating navmeshes for larger or + // more complicated geometry. + firstBaseTileLinkUsed = true; + firstLandTileLinkUsed = true; + + // Calculate the portal limits. + + dtLink* const forwardLink = &baseTile->links[forwardIdx]; + + forwardLink->ref = landPolyRef; + forwardLink->edge = (unsigned char)j; + forwardLink->side = landSide; + forwardLink->bmin = (unsigned char)rdMathRoundf(baseTmin * 255.f); + forwardLink->bmax = (unsigned char)rdMathRoundf(baseTmax * 255.f); + forwardLink->next = basePoly->firstLink; + basePoly->firstLink = forwardIdx; + forwardLink->traverseType = (unsigned char)traverseType; + forwardLink->traverseDist = quantDist; + forwardLink->reverseLink = (unsigned short)reverseIdx; + + dtLink* const reverseLink = &landTile->links[reverseIdx]; + + reverseLink->ref = basePolyRef; + reverseLink->edge = (unsigned char)p; + reverseLink->side = baseSide; + reverseLink->bmin = (unsigned char)rdMathRoundf(landTmin * 255.f); + reverseLink->bmax = (unsigned char)rdMathRoundf(landTmax * 255.f); + reverseLink->next = landPoly->firstLink; + landPoly->firstLink = reverseIdx; + reverseLink->traverseType = (unsigned char)traverseType; + reverseLink->traverseDist = quantDist; + reverseLink->reverseLink = (unsigned short)forwardIdx; + + if (traverseLinkFound) + linkedIt->second |= 1 << traverseType; + else + m_traverseLinkPolyMap.emplace(linkedPolyPair, 1 << traverseType); + } } } - - if (!traverseTypeSupported) - continue; } - else - { - const int traverseTableIndex = NavMesh_GetFirstTraverseAnimTypeForType(m_selectedNavMeshType); - const bool traverseTypeSupported = rdBitCellBit(traverseType) & s_traverseAnimTraverseFlags[traverseTableIndex]; - - if (!traverseTypeSupported) - continue; - } - - const dtPolyRef basePolyRef = basePolyRefBase | i; - const dtPolyRef landPolyRef = landPolyRefBase | l; - - const TraverseLinkPolyPair linkedPolyPair(basePolyRef, landPolyRef); - auto linkedIt = m_traverseLinkPolyMap.find(linkedPolyPair); - - bool traverseLinkFound = false; - - if (linkedIt != m_traverseLinkPolyMap.end()) - traverseLinkFound = true; - - // These 2 polygons are already linked with the same traverse type. - if (traverseLinkFound && (rdBitCellBit(traverseType) & linkedIt->second)) - continue; - - const bool basePolyHigher = basePolyEdgeMid[2] > landPolyEdgeMid[2]; - float* const lowerEdgeMid = basePolyHigher ? landPolyEdgeMid : basePolyEdgeMid; - float* const higherEdgeMid = basePolyHigher ? basePolyEdgeMid : landPolyEdgeMid; - float* const lowerEdgeDir = basePolyHigher ? landEdgeDir : baseEdgeDir; - float* const higherEdgeDir = basePolyHigher ? baseEdgeDir : landEdgeDir; - - const float walkableRadius = basePolyHigher ? baseHeader->walkableRadius : landHeader->walkableRadius; - float offsetAmount; - - if (m_traverseRayDynamicOffset) - { - const float totLedgeSpan = walkableRadius+m_traverseRayExtraOffset; - const float maxAngle = rdCalcMaxLOSAngle(totLedgeSpan, m_cellHeight); - - offsetAmount = rdCalcLedgeSpanOffsetAmount(totLedgeSpan, slopeAngle, maxAngle); - } - else - offsetAmount = walkableRadius + m_traverseRayExtraOffset; - - if (!traverseLinkInLOS(m_geom, lowerEdgeMid, higherEdgeMid, lowerEdgeDir, higherEdgeDir, offsetAmount)) - continue; - - const unsigned char landSide = linkToNeighbor - ? rdClassifyPointOutsideBounds(landPolyEdgeMid, landHeader->bmin, landHeader->bmax) - : rdClassifyPointInsideBounds(landPolyEdgeMid, landHeader->bmin, landHeader->bmax); - - const unsigned int forwardIdx = baseTile->allocLink(); - const unsigned int reverseIdx = landTile->allocLink(); - - // Allocated 2 new links, need to check for enough space on subsequent runs. - // This optimization saves a lot of time generating navmeshes for larger or - // more complicated geometry. - firstBaseTileLinkUsed = true; - firstLandTileLinkUsed = true; - - dtLink* const forwardLink = &baseTile->links[forwardIdx]; - - forwardLink->ref = landPolyRef; - forwardLink->edge = (unsigned char)j; - forwardLink->side = landSide; - forwardLink->bmin = 0; - forwardLink->bmax = 255; - forwardLink->next = basePoly->firstLink; - basePoly->firstLink = forwardIdx; - forwardLink->traverseType = (unsigned char)traverseType; - forwardLink->traverseDist = quantDist; - forwardLink->reverseLink = (unsigned short)reverseIdx; - - dtLink* const reverseLink = &landTile->links[reverseIdx]; - - reverseLink->ref = basePolyRef; - reverseLink->edge = (unsigned char)m; - reverseLink->side = baseSide; - reverseLink->bmin = 0; - reverseLink->bmax = 255; - reverseLink->next = landPoly->firstLink; - landPoly->firstLink = reverseIdx; - reverseLink->traverseType = (unsigned char)traverseType; - reverseLink->traverseDist = quantDist; - reverseLink->reverseLink = (unsigned short)forwardIdx; - - if (traverseLinkFound) - linkedIt->second |= 1 << traverseType; - else - m_traverseLinkPolyMap.emplace(linkedPolyPair, 1 << traverseType); } } }