How can I generate a map/minmap using the navmesh?

Hello,

I’m interested in making a Diablo/Torchlight style map/minimap. I’m not sure how to approach it. One way I thought about it is using the SceneCaptureComponent2D use a 2D hand drawn texture of the level, but that doesn’t work for me because my dungeon is generated randomly with premade tiles and I don’t want to draw a map for every tile, and if I decide to add new tiles I have to draw again. Another way I was thinking about was to make another camera with an outline post-processing effect on it and use that, but it still seems like a cheap solution and I don’t know if its costly. I came across a couple of posts here on using navmesh with a texture to achieve the desired effect but I’m not sure if it is possible and how to even achieve it since they had no answers.

Thanks in advance!

I’ve been looking for a similar answer, on and off, for months … still no solid option.

I found a solution to this! Using C++, you can extract the vertices and indices of the NavMesh and bake them into a static mesh.

Step 1: Create a new C++ Blueprint Function Library Class.
Step 2: Add this to the header:

UFUNCTION(BlueprintCallable, Category="Minimap|NavMesh", meta=(WorldContext="WorldContextObject"))
static void BakeNavMeshToStaticMesh(
	UObject* WorldContextObject,
	const FString& AssetPackagePath = TEXT("/Game/"), bool
	bDoubleSided = false
	);

Step 3: Add this to the .cpp file:

void UMinimapFunctionLibrary::BakeNavMeshToStaticMesh(
    UObject* WorldContextObject,
    const FString& AssetPackagePath,
    bool bDoubleSided
)
{
    // If we cannot get a reference to the RecastNavMesh, return
    if (!WorldContextObject) return;
    UWorld* World = WorldContextObject->GetWorld();
    if (!World) return;
    UNavigationSystemV1* NavigationSystem = FNavigationSystem::GetCurrent<UNavigationSystemV1>(World);
    if (!NavigationSystem) return;
    ARecastNavMesh* RecastNavMesh = Cast<ARecastNavMesh>(NavigationSystem->GetMainNavData());
    if (!RecastNavMesh) return;

    // Get and loop through all tile references on the NavMesh
    TArray<FNavTileRef> TileRefs;
    RecastNavMesh->GetAllNavMeshTiles(TileRefs);
    
    TArray<FVector> AllVertices;
    TArray<int32> AllIndices;
    
    for (FNavTileRef TileRef : TileRefs)
    {
        FBox Bounds = RecastNavMesh->GetNavMeshTileBounds(TileRef);
        if (!Bounds.IsValid) continue;

        TArray<FNavPoly> Polys;
        RecastNavMesh->GetPolysInTile(TileRef, Polys);

        // For each polygon, extract all vertices and triangulate them
        for (const FNavPoly& Poly : Polys)
        {
            TArray<FVector> PolyVerts;
            if (!RecastNavMesh->GetPolyVerts(Poly.Ref, PolyVerts) || PolyVerts.Num() < 3) continue;

            int32 BaseIndex = AllVertices.Num();
            AllVertices.Append(PolyVerts);

            for (int32 i = 1; i < PolyVerts.Num() - 1; ++i)
            {
                AllIndices.Add(BaseIndex + 0);
                AllIndices.Add(BaseIndex + i);
                AllIndices.Add(BaseIndex + (i + 1));
            }
        }
    }

    // If nothing was extracted, return
    if (AllVertices.Num() == 0 || AllIndices.Num() == 0) return;
    
    // Create a new static mesh asset
    FString PackageRoot, PackagePath, AssetName;
    FPackageName::SplitLongPackageName(
        AssetPackagePath,
        PackageRoot,
        PackagePath,
        AssetName,
        false
    );

    FString Folder = PackageRoot + PackagePath;
    FString FullPackage = Folder + AssetName;

    UPackage* Package = CreatePackage(*FullPackage);
    Package->FullyLoad();

    UStaticMesh* NewMesh = NewObject<UStaticMesh>(Package, *AssetName, RF_Public | RF_Standalone);
    if (!NewMesh) return;
    
    NewMesh->SetNumSourceModels(1);
    NewMesh->SetLightingGuid(FGuid::NewGuid());
    NewMesh->GetStaticMaterials().Add(FStaticMaterial());
    NewMesh->CreateMeshDescription(0);
    FMeshDescription& MeshDesc = *NewMesh->GetMeshDescription(0);

    FStaticMeshAttributes Attributes(MeshDesc);
    Attributes.Register();

    TArray<FVertexID> VertexIDs;
    VertexIDs.Reserve(AllVertices.Num());
    for (const FVector& Pos : AllVertices)
    {
        FVertexID VID = MeshDesc.CreateVertex();
        Attributes.GetVertexPositions()[VID] = static_cast<FVector3f>(Pos);
        VertexIDs.Add(VID);
    }

    const FPolygonGroupID PolyGroup = MeshDesc.CreatePolygonGroup();

    // For each index-triplet, create a polygon
    for (int32 TriIdx = 0; TriIdx < AllIndices.Num(); TriIdx += 3)
    {
        int32 I0 = AllIndices[TriIdx];
        int32 I1 = AllIndices[TriIdx + 1];
        int32 I2 = AllIndices[TriIdx + 2];

        TArray<FVertexInstanceID> CornerIDs;
        CornerIDs.SetNum(3);
        CornerIDs[0] = MeshDesc.CreateVertexInstance(VertexIDs[I0]);
        CornerIDs[1] = MeshDesc.CreateVertexInstance(VertexIDs[I1]);
        CornerIDs[2] = MeshDesc.CreateVertexInstance(VertexIDs[I2]);
        MeshDesc.CreatePolygon(PolyGroup, CornerIDs);

        if (bDoubleSided)
        {
            TArray<FVertexInstanceID> BackIDs;
            BackIDs.SetNum(3);
            BackIDs[0] = MeshDesc.CreateVertexInstance(VertexIDs[I0]);
            BackIDs[1] = MeshDesc.CreateVertexInstance(VertexIDs[I2]);
            BackIDs[2] = MeshDesc.CreateVertexInstance(VertexIDs[I1]);
            MeshDesc.CreatePolygon(PolyGroup, BackIDs);
        }
    }
    
    NewMesh->CommitMeshDescription(0);
    NewMesh->Build(false);
    NewMesh->PostEditChange();

    FAssetRegistryModule::AssetCreated(NewMesh);
    Package->MarkPackageDirty();
}

Step 4: Call this function in an Editor Utility Widget.

→ This will generate a 3D static mesh in the shape of your NavMesh. I then add it to an actor with a SceneCaptureComponent 2D, set it to ShowOnlyActorComponents, and set the Mesh to SetVisibleInSceneCaptureOnly.

Hope this helps.