// Copyright IHY. Vertical Partition Streaming plugin (editor module). // // Installs the runtime->editor bridge so the volume's CallInEditor buttons and the // vp.* build console commands reach UVerticalPartitionBuilder. Also implements the // editor-only chunk preview. #include "Modules/ModuleManager.h" #include "VerticalPartitionVolume.h" #include "VerticalPartitionBuilder.h" #include "VerticalPartitionDescriptor.h" #include "VerticalPartitionStatics.h" #include "VerticalPartitionLog.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "Engine/StaticMesh.h" #include "Engine/StaticMeshActor.h" #include "Components/StaticMeshComponent.h" #include "DrawDebugHelpers.h" #include "EngineUtils.h" #include "Misc/PackageName.h" #include "UObject/Package.h" static const FName VPPreviewTag(TEXT("VPHLODPreview")); #if WITH_EDITOR #include "Editor.h" #endif namespace { void LogReport(const FVerticalPartitionBuildReport& R, const TCHAR* Title) { UE_LOG(LogVerticalPartition, Log, TEXT("[VP] %s: scanned=%d inside=%d excluded=%d cells=%d empty=%d warn=%d err=%d largest=%s(%d)"), Title, R.TotalActorsScanned, R.ActorsInsideVolume, R.ActorsExcluded, R.GeneratedCells, R.EmptyCells, R.WarningCount, R.ErrorCount, *R.LargestCell.ToString(), R.LargestCellActorCount); if (GEngine) { GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Cyan, FString::Printf(TEXT("VP %s: %d cells, %d warnings, %d errors"), Title, R.GeneratedCells, R.WarningCount, R.ErrorCount)); } for (const FVerticalPartitionWarning& W : R.Warnings) { UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] WARN [%s] %s"), *UVerticalPartitionStatics::WarningTypeToString(W.Type), *W.Message); } for (const FVerticalPartitionError& E : R.Errors) { UE_LOG(LogVerticalPartition, Error, TEXT("[VP] ERR [%s] %s"), *UVerticalPartitionStatics::WarningTypeToString(E.Type), *E.Message); } } // Persistent coloured cell boxes. GREEN = cell has a baked HLOD proxy, ORANGE = cell // has streamed actors but no proxy (it will just hide when far), GREY = empty Z slice. void PreviewChunks(AVerticalPartitionVolume* Volume) { #if WITH_EDITOR if (!Volume || !GEditor) { return; } UWorld* World = GEditor->GetEditorWorldContext().World(); if (!World) { return; } const FVerticalPartitionSettings& S = Volume->Settings; const FBox Bounds = Volume->GetVolumeBounds(); UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous(); FlushPersistentDebugLines(World); DrawDebugBox(World, Bounds.GetCenter(), Bounds.GetExtent(), FColor::Cyan, true, -1.f, 0, 30.f); const int32 NumZ = UVerticalPartitionStatics::GetNumZCells(Bounds, S); int32 CellsWithHLOD = 0; for (int32 Z = 0; Z < NumZ; ++Z) { const FVerticalCellId Id(0, 0, Z); const FBox CB = UVerticalPartitionStatics::GetCellBounds(Id, Bounds, S); int32 Actors = 0; bool bHasCell = false; bool bHasHLOD = false; if (Desc) { for (const FVerticalCellDescriptor& CD : Desc->Cells) { if (CD.CellId.Z == Z) { bHasCell = true; Actors += CD.ActorCount; if (CD.HLODProxyAsset.IsValid()) { bHasHLOD = true; } } } } if (bHasHLOD) { ++CellsWithHLOD; } const FColor Color = !bHasCell ? FColor(90, 90, 90) : (bHasHLOD ? FColor(40, 200, 80) : FColor(235, 140, 30)); DrawDebugBox(World, CB.GetCenter(), CB.GetExtent(), Color, true, -1.f, 0, 12.f); const FString Label = bHasCell ? FString::Printf(TEXT("Z%d %s actors:%d"), Z, bHasHLOD ? TEXT("[HLOD]") : TEXT("[no-HLOD]"), Actors) : FString::Printf(TEXT("Z%d (empty/persistent)"), Z); DrawDebugString(World, CB.GetCenter(), Label, nullptr, Color, -1.f, true); } UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Preview: %d Z cells (%s). GREEN=HLOD proxy, ORANGE=streamed/no-proxy, GREY=empty. %d cells have a proxy. Use 'Clear Preview' to remove."), NumZ, Desc ? TEXT("from descriptor") : TEXT("no descriptor yet — Build first for HLOD colours"), CellsWithHLOD); #endif } int32 ClearPreviewActors(UWorld* World) { int32 Removed = 0; #if WITH_EDITOR if (!World) { return 0; } for (TActorIterator It(World); It; ++It) { if (It->ActorHasTag(VPPreviewTag)) { It->Destroy(); ++Removed; } } #endif return Removed; } void ClearPreview(AVerticalPartitionVolume* Volume) { #if WITH_EDITOR if (!GEditor) { return; } UWorld* World = GEditor->GetEditorWorldContext().World(); if (!World) { return; } FlushPersistentDebugLines(World); const int32 Removed = ClearPreviewActors(World); UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Preview cleared (%d HLOD preview actors removed)."), Removed); #endif } // Spawn the baked proxy meshes as temporary editor actors so the HLOD geometry is // directly visible in the viewport. They sit exactly over the source (pivot-at-zero), // so hide the real decor to compare. Tagged + transient; removed by Clear Preview. void SpawnHLODPreview(AVerticalPartitionVolume* Volume) { #if WITH_EDITOR if (!Volume || !GEditor) { return; } UWorld* World = GEditor->GetEditorWorldContext().World(); if (!World) { return; } UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous(); if (!Desc) { UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] SpawnHLODPreview: no descriptor — run Build first.")); return; } ClearPreviewActors(World); int32 Spawned = 0; for (const FVerticalCellDescriptor& CD : Desc->Cells) { if (!CD.HLODProxyAsset.IsValid()) { continue; } UStaticMesh* Mesh = Cast(CD.HLODProxyAsset.TryLoad()); if (!Mesh) { continue; } FActorSpawnParameters Params; Params.ObjectFlags |= RF_Transient; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; AStaticMeshActor* Actor = World->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, Params); if (!Actor) { continue; } Actor->Tags.Add(VPPreviewTag); Actor->SetActorLabel(FString::Printf(TEXT("VP_HLODPreview_Z%d"), CD.CellId.Z)); if (UStaticMeshComponent* Comp = Actor->GetStaticMeshComponent()) { Comp->SetMobility(EComponentMobility::Movable); Comp->SetStaticMesh(Mesh); Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision); } ++Spawned; } UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Spawned %d HLOD proxy preview actors (tag '%s'). Hide your static decor to compare; 'Clear Preview' removes them."), Spawned, *VPPreviewTag.ToString()); if (GEngine) { GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Green, FString::Printf(TEXT("VP: %d HLOD proxy preview actors spawned"), Spawned)); } #endif } void HandleAction(AVerticalPartitionVolume* Volume, EVerticalPartitionAction Action) { if (!Volume) { return; } FVerticalPartitionBuildReport Report; switch (Action) { case EVerticalPartitionAction::Analyze: Report = UVerticalPartitionBuilder::AnalyzeCurrentLevel(Volume); LogReport(Report, TEXT("Analyze")); break; case EVerticalPartitionAction::Build: UVerticalPartitionBuilder::Build(Volume, Report); LogReport(Report, TEXT("Build")); break; case EVerticalPartitionAction::RebuildChangedCells: UVerticalPartitionBuilder::RebuildChangedCells(Volume, Report); LogReport(Report, TEXT("Rebuild")); break; case EVerticalPartitionAction::RebuildHLOD: UVerticalPartitionBuilder::RebuildHLOD(Volume, Report); LogReport(Report, TEXT("RebuildHLOD")); break; case EVerticalPartitionAction::Validate: Report = UVerticalPartitionBuilder::ValidateCells(Volume); LogReport(Report, TEXT("Validate")); break; case EVerticalPartitionAction::PreviewChunks: PreviewChunks(Volume); break; case EVerticalPartitionAction::SpawnHLODPreview: SpawnHLODPreview(Volume); break; case EVerticalPartitionAction::ClearPreview: ClearPreview(Volume); break; case EVerticalPartitionAction::ClearGenerated: UVerticalPartitionBuilder::ClearGenerated(Volume); break; case EVerticalPartitionAction::Commit: UVerticalPartitionBuilder::CommitConversion(Volume, Report); LogReport(Report, TEXT("Commit")); break; case EVerticalPartitionAction::RestoreBackup: UVerticalPartitionBuilder::RestoreFromBackup(Volume->GetWorld()); break; case EVerticalPartitionAction::DumpReport: if (UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous()) { const FString Map = Volume->GetWorld() ? FPackageName::GetShortName(Volume->GetWorld()->GetOutermost()->GetName()) : TEXT("Map"); UVerticalPartitionBuilder::DumpReport(Desc->LastReport, Map); } break; } } } class FVerticalPartitionEditorModule : public IModuleInterface { public: virtual void StartupModule() override { #if WITH_EDITOR FVerticalPartitionEditorBridge::Handler = [](AVerticalPartitionVolume* Volume, EVerticalPartitionAction Action) { HandleAction(Volume, Action); }; #endif } virtual void ShutdownModule() override { #if WITH_EDITOR FVerticalPartitionEditorBridge::Handler = nullptr; #endif } }; IMPLEMENT_MODULE(FVerticalPartitionEditorModule, VerticalPartitionEditor);