// Copyright IHY. Vertical Partition Streaming plugin (editor module). #include "VerticalPartitionBuilder.h" #include "VerticalPartitionVolume.h" #include "VerticalPartitionDescriptor.h" #include "VerticalPartitionManager.h" #include "VerticalPartitionStatics.h" #include "VerticalPartitionSettings.h" #include "VerticalPartitionLog.h" #include "EngineUtils.h" #include "Engine/World.h" #include "Engine/Level.h" #include "Engine/StaticMesh.h" #include "Engine/StaticMeshActor.h" #include "Engine/BlueprintGeneratedClass.h" #include "GameFramework/Actor.h" #include "Components/StaticMeshComponent.h" #include "Components/PrimitiveComponent.h" #include "Components/LightComponentBase.h" #include "Components/AudioComponent.h" #include "Components/SkyLightComponent.h" #include "Components/SkyAtmosphereComponent.h" #include "Components/ExponentialHeightFogComponent.h" #include "Components/PostProcessComponent.h" #include "AssetRegistry/AssetRegistryModule.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" #include "Misc/PackageName.h" #include "Misc/Paths.h" #include "Misc/FileHelper.h" #include "Misc/DateTime.h" #include "HAL/FileManager.h" #include "MeshMerge/MeshMergingSettings.h" #include "MeshMerge/MeshProxySettings.h" #include "IMeshMergeUtilities.h" #include "MeshMergeModule.h" #include "IMeshReductionInterfaces.h" #include "LevelInstance/LevelInstanceInterface.h" #include "LevelInstance/LevelInstanceSubsystem.h" #include "PackedLevelActor/PackedLevelActor.h" #include "Misc/Guid.h" #if WITH_EDITOR #include "Editor.h" #endif // --------------------------------------------------------------------------- // Analyze // --------------------------------------------------------------------------- FVerticalPartitionBuildReport UVerticalPartitionBuilder::AnalyzeCurrentLevel(AVerticalPartitionVolume* Volume) { FVerticalPartitionBuildReport Report; if (!Volume || !Volume->GetWorld()) { FVerticalPartitionError E; E.Type = EVerticalWarningType::NoVolume; E.Message = TEXT("No valid AVerticalPartitionVolume / world to analyze."); Report.Errors.Add(E); Report.ErrorCount = 1; return Report; } const FVerticalPartitionSettings& S = Volume->Settings; const FBox Bounds = Volume->GetVolumeBounds(); TArray Assignments; GatherActors(Volume, Bounds, Assignments, Report); TMap CellCounts; for (const FVerticalActorAssignment& A : Assignments) { if (A.bExcluded) { continue; } ClassifyActorWarnings(A, S, Report); CellCounts.FindOrAdd(A.Cell)++; } Report.GeneratedCells = CellCounts.Num(); for (const TPair& Pair : CellCounts) { if (Pair.Value > Report.LargestCellActorCount) { Report.LargestCellActorCount = Pair.Value; Report.LargestCell = Pair.Key; } if (Pair.Value > 2000) { FVerticalPartitionWarning W; W.Type = EVerticalWarningType::CellTooManyActors; W.Cell = Pair.Key; W.Message = FString::Printf(TEXT("Cell %s has %d actors."), *Pair.Key.ToString(), Pair.Value); Report.Warnings.Add(W); } } Report.WarningCount = Report.Warnings.Num(); Report.ErrorCount = Report.Errors.Num(); return Report; } // An actor is safe to STREAM (hide/show + proxy) only if it is pure static decor. // Anything that lights, sounds, moves, replicates, or is a Level Instance container // must stay in the persistent level — hiding it would break the scene (e.g. the sky). static bool IsStreamableStaticDecor(AActor* A) { if (!A) { return false; } // Must actually own static geometry. Level Instances have no mesh components on the // actor itself, so they fail here and are kept persistent. bool bHasStatic = false; TArray SM; A->GetComponents(SM); for (UStaticMeshComponent* C : SM) { if (C && C->GetStaticMesh() && C->Mobility == EComponentMobility::Static) { bHasStatic = true; break; } } if (!bHasStatic) { return false; } // Disqualifying components — never hide lighting / sky / audio. if (A->FindComponentByClass()) { return false; } if (A->FindComponentByClass()) { return false; } if (A->FindComponentByClass()) { return false; } if (A->FindComponentByClass()) { return false; } if (A->FindComponentByClass()) { return false; } if (A->FindComponentByClass()) { return false; } // Name-based sweep for FX / capture / decal (avoids pulling extra modules). Erring // toward "keep persistent" is always safe; it just streams a little less. for (UActorComponent* Comp : A->GetComponents()) { if (!Comp) { continue; } const FString CN = Comp->GetClass()->GetName(); if (CN.Contains(TEXT("Light")) || CN.Contains(TEXT("Fog")) || CN.Contains(TEXT("Sky")) || CN.Contains(TEXT("Particle")) || CN.Contains(TEXT("Niagara")) || CN.Contains(TEXT("ReflectionCapture")) || CN.Contains(TEXT("Decal")) || CN.Contains(TEXT("PostProcess"))) { return false; } } // Gameplay / dynamic signals. if (A->GetIsReplicated()) { return false; } if (USceneComponent* Root = A->GetRootComponent()) { if (Root->Mobility == EComponentMobility::Movable) { return false; } } TArray Prims; A->GetComponents(Prims); for (UPrimitiveComponent* P : Prims) { if (P && P->IsSimulatingPhysics()) { return false; } } return true; } // Flatten a streamable unit to its renderable leaf actors: // - a normal actor or a Packed Level Actor -> itself (its own static / ISM / HISM comps), // - a plain Level Instance -> its loaded child actors, recursively (nested LIs flattened, // Packed Level Actors kept whole). Lights / gameplay leaves are returned too; the caller // filters with IsStreamableStaticDecor so only static decor is actually streamed. static void CollectStreamLeafActors(AActor* Unit, UWorld* World, TArray& Out, int32 Depth = 0) { if (!Unit || Depth > 8) { return; } // A Packed Level Actor bakes its source level into owned ISM/HISM components, so it is a // leaf (do NOT recurse into its source world). A plain Level Instance streams a real level // whose actors we recurse into. const bool bIsPacked = Unit->IsA(APackedLevelActor::StaticClass()); ILevelInstanceInterface* LI = bIsPacked ? nullptr : Cast(Unit); if (LI && World) { if (ULevelInstanceSubsystem* Sub = World->GetSubsystem()) { if (LI->IsLoaded()) { Sub->ForEachActorInLevelInstance(LI, [&](AActor* Child) -> bool { if (Child && Child != Unit) { CollectStreamLeafActors(Child, World, Out, Depth + 1); } return true; // keep iterating }); return; } } } Out.AddUnique(Unit); } void UVerticalPartitionBuilder::GatherActors(AVerticalPartitionVolume* Volume, const FBox& VolumeBounds, TArray& OutAssignments, FVerticalPartitionBuildReport& Report) { UWorld* World = Volume->GetWorld(); const FVerticalPartitionSettings& S = Volume->Settings; auto IsExcludedClass = [&S](AActor* A) -> bool { for (const TSoftClassPtr& Soft : S.ExcludeClasses) { if (UClass* C = Soft.LoadSynchronous()) { if (A->IsA(C)) { return true; } } } if (S.IncludeOnlyClasses.Num() > 0) { bool bIncluded = false; for (const TSoftClassPtr& Soft : S.IncludeOnlyClasses) { if (UClass* C = Soft.LoadSynchronous()) { if (A->IsA(C)) { bIncluded = true; break; } } } if (!bIncluded) { return true; } } return false; }; ULevelInstanceSubsystem* LISub = World ? World->GetSubsystem() : nullptr; for (TActorIterator It(World); It; ++It) { AActor* A = *It; if (!A || A == Volume || A->IsA() || A->IsA()) { continue; } if (A->IsEditorOnly()) { continue; } // Skip actors that are the CONTENT of a Level Instance — they are represented by // (and streamed through) their owning LI unit, so they must not be scanned twice. if (LISub && A->GetLevel() && LISub->GetOwningLevelInstance(A->GetLevel())) { continue; } ++Report.TotalActorsScanned; FBox B = A->GetComponentsBoundingBox(true); if (!B.IsValid) { const FVector P = A->GetActorLocation(); B = FBox(P - FVector(50.f), P + FVector(50.f)); } const bool bInside = !S.bIncludeOnlyActorsInsideVolume || VolumeBounds.Intersect(B); if (!bInside) { continue; } ++Report.ActorsInsideVolume; FVerticalActorAssignment Asg; Asg.Actor = A; Asg.Bounds = B; if (IsExcludedClass(A)) { Asg.bExcluded = true; ++Report.ActorsExcluded; OutAssignments.Add(Asg); continue; } // Flatten this unit (normal actor / Packed Level Actor / Level Instance) to its // streamable static-decor leaf actors. Lights / gameplay leaves are dropped here and // stay persistent + untouched. This is what makes LI + PLA support universal. const bool bIsLevelInstance = (Cast(A) != nullptr); const bool bIsPackedLI = A->IsA(APackedLevelActor::StaticClass()); if (bIsLevelInstance && !bIsPackedLI && !S.bStreamLevelInstances) { Asg.bExcluded = true; ++Report.ActorsExcluded; OutAssignments.Add(Asg); continue; } TArray RawLeaves; CollectStreamLeafActors(A, World, RawLeaves); FBox UnitBounds(ForceInit); for (AActor* Leaf : RawLeaves) { if (S.bStreamOnlyStaticDecor && !IsStreamableStaticDecor(Leaf)) { continue; // keep lights/audio/dynamic content persistent } Asg.LeafActors.Add(Leaf); UnitBounds += Leaf->GetComponentsBoundingBox(true); } if (Asg.LeafActors.Num() == 0) { // Nothing safe to stream from this unit -> keep persistent. if (bIsLevelInstance) { if (ILevelInstanceInterface* LI = Cast(A)) { if (!LI->IsLoaded()) { FVerticalPartitionWarning W; W.Type = EVerticalWarningType::ActorNeedsManualReview; W.Actor = FSoftObjectPath(A); W.Message = FString::Printf(TEXT("Level Instance '%s' is not loaded — load it in the editor and rebuild to stream / HLOD it."), *A->GetName()); Report.Warnings.Add(W); } } } Asg.bExcluded = true; ++Report.ActorsExcluded; OutAssignments.Add(Asg); continue; } // Assign by the actual streamed geometry's bounds (handles LIs whose actor pivot // differs from their contents). if (UnitBounds.IsValid) { B = UnitBounds; Asg.Bounds = UnitBounds; } if (S.bUseActorBoundsInsteadOfOrigin) { Asg.Cell = UVerticalPartitionStatics::GetCellIdFromBounds(B, VolumeBounds, S, Asg.bCrossesMultiple); } else { Asg.Cell = UVerticalPartitionStatics::GetCellIdFromPoint(B.GetCenter(), VolumeBounds, S); } // Large-actor handling. const float ZExtent = B.Max.Z - B.Min.Z; const bool bLarge = (S.CellHeight > KINDA_SMALL_NUMBER) && (ZExtent > S.CellHeight * S.LargeActorCellSpan); if (bLarge) { switch (S.LargeActorPolicy) { case ELargeActorPolicy::KeepInPersistentLevel: case ELargeActorPolicy::DuplicateAsHLODOnly: Asg.bLargeKeepPersistent = true; break; case ELargeActorPolicy::ExcludeFromPartition: Asg.bExcluded = true; ++Report.ActorsExcluded; break; case ELargeActorPolicy::SplitNotSupportedWarning: case ELargeActorPolicy::AssignToDominantCell: default: break; // assigned to dominant (centre) cell already } FVerticalPartitionWarning W; W.Type = EVerticalWarningType::ActorTooLarge; W.Actor = FSoftObjectPath(A); W.Cell = Asg.Cell; W.Message = FString::Printf(TEXT("%s spans %.0f cm (> %.1f cells)."), *A->GetName(), ZExtent, S.LargeActorCellSpan); Report.Warnings.Add(W); } OutAssignments.Add(Asg); } } void UVerticalPartitionBuilder::ClassifyActorWarnings(const FVerticalActorAssignment& A, const FVerticalPartitionSettings& /*S*/, FVerticalPartitionBuildReport& Report) { AActor* Actor = A.Actor.Get(); if (!Actor) { return; } const FSoftObjectPath Path(Actor); auto AddW = [&](EVerticalWarningType Type, const FString& Msg) { FVerticalPartitionWarning W; W.Type = Type; W.Actor = Path; W.Cell = A.Cell; W.Message = Msg; Report.Warnings.Add(W); }; if (A.bCrossesMultiple) { AddW(EVerticalWarningType::ActorCrossesMultipleCells, FString::Printf(TEXT("%s straddles a cell boundary; assigned to dominant cell %s."), *Actor->GetName(), *A.Cell.ToString())); } // Physics simulation. bool bPhysics = false; bool bMovable = false; bool bLight = false; bool bAudio = false; TArray Prims; Actor->GetComponents(Prims); for (UPrimitiveComponent* P : Prims) { if (P && P->IsSimulatingPhysics()) { bPhysics = true; } if (P && P->Mobility == EComponentMobility::Movable) { bMovable = true; } } if (USceneComponent* Root = Actor->GetRootComponent()) { if (Root->Mobility == EComponentMobility::Movable) { bMovable = true; } } TArray Lights; Actor->GetComponents(Lights); bLight = Lights.Num() > 0; TArray Audios; Actor->GetComponents(Audios); bAudio = Audios.Num() > 0; if (bPhysics) { AddW(EVerticalWarningType::ActorSimulatesPhysics, FString::Printf(TEXT("%s simulates physics; keep in persistent level."), *Actor->GetName())); } if (bMovable) { AddW(EVerticalWarningType::ActorDynamicMobility, FString::Printf(TEXT("%s has Movable mobility."), *Actor->GetName())); } if (bLight || bAudio) { AddW(EVerticalWarningType::ActorHasAudioLightNav, FString::Printf(TEXT("%s has %s%s components."), *Actor->GetName(), bLight ? TEXT("light ") : TEXT(""), bAudio ? TEXT("audio") : TEXT(""))); } if (Actor->GetIsReplicated()) { AddW(EVerticalWarningType::ActorNotStreamSafe, FString::Printf(TEXT("%s is replicated; streaming it per-client may desync."), *Actor->GetName())); } if (Cast(Actor->GetClass()) && Actor->PrimaryActorTick.bCanEverTick) { AddW(EVerticalWarningType::ActorHasBlueprintTick, FString::Printf(TEXT("%s is a ticking Blueprint actor."), *Actor->GetName())); } } // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- bool UVerticalPartitionBuilder::Build(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport) { OutReport.Reset(); if (!Volume || !Volume->GetWorld()) { return false; } UWorld* World = Volume->GetWorld(); const FVerticalPartitionSettings& S = Volume->Settings; const FBox Bounds = Volume->GetVolumeBounds(); if (S.bCreateBackup) { FString BackupPath; CreateBackup(World, BackupPath); } TArray Assignments; GatherActors(Volume, Bounds, Assignments, OutReport); for (const FVerticalActorAssignment& A : Assignments) { if (!A.bExcluded) { ClassifyActorWarnings(A, S, OutReport); } } UVerticalPartitionDescriptor* Desc = CreateOrLoadDescriptor(Volume); if (!Desc) { FVerticalPartitionError E; E.Type = EVerticalWarningType::PackageSaveFailed; E.Message = TEXT("Failed to create descriptor asset."); OutReport.Errors.Add(E); OutReport.ErrorCount = OutReport.Errors.Num(); return false; } Desc->Settings = S; Desc->VolumeBounds = Bounds; Desc->BuiltMode = S.ConversionMode; Desc->SourceLevel = FSoftObjectPath(World->GetOutermost()->GetName()); BuildCells(Volume, Bounds, Assignments, Desc, OutReport); Desc->SourceContentHash = ComputeContentHash(Assignments); OutReport.WarningCount = OutReport.Warnings.Num(); OutReport.ErrorCount = OutReport.Errors.Num(); Desc->LastReport = OutReport; SaveAsset(Desc); // Link descriptor to volume + ensure a manager exists. Volume->Descriptor = Desc; Volume->MarkPackageDirty(); bool bHasManager = false; for (TActorIterator It(World); It; ++It) { bHasManager = true; break; } if (!bHasManager) { if (AVerticalPartitionManager* Mgr = World->SpawnActor()) { Mgr->Volume = Volume; Mgr->Descriptor = Desc; Mgr->MarkPackageDirty(); } } if (S.ConversionMode == EVerticalConversionMode::DestructiveCommit) { CommitConversion(Volume, OutReport); } UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Build complete: %d cells, %d warnings, %d errors."), OutReport.GeneratedCells, OutReport.WarningCount, OutReport.ErrorCount); return true; } void UVerticalPartitionBuilder::BuildCells(AVerticalPartitionVolume* Volume, const FBox& VolumeBounds, const TArray& Assignments, UVerticalPartitionDescriptor* Descriptor, FVerticalPartitionBuildReport& Report) { const FVerticalPartitionSettings& S = Volume->Settings; const FString PackageRoot = S.GeneratedPackageRoot; // Group assignments by cell (skip excluded / persistent-kept). TMap> CellActors; for (const FVerticalActorAssignment& A : Assignments) { if (A.bExcluded || A.bLargeKeepPersistent) { continue; } TArray& Bucket = CellActors.FindOrAdd(A.Cell); for (const TWeakObjectPtr& Leaf : A.LeafActors) { if (AActor* L = Leaf.Get()) { Bucket.Add(L); } } } Descriptor->Cells.Reset(); Descriptor->Cells.Reserve(CellActors.Num()); for (TPair>& Pair : CellActors) { FVerticalCellDescriptor CD; CD.CellId = Pair.Key; CD.Bounds = UVerticalPartitionStatics::GetCellBounds(Pair.Key, VolumeBounds, S); for (AActor* A : Pair.Value) { if (!A) { continue; } CD.SourceActors.Add(FSoftObjectPath(A)); ++CD.ActorCount; TArray SMComps; A->GetComponents(SMComps); if (SMComps.Num() > 0) { ++CD.StaticMeshCount; CD.EstimatedMemoryMB += 0.25f * SMComps.Num(); } if (Cast(A->GetClass())) { ++CD.BlueprintActorCount; } TArray Lights; A->GetComponents(Lights); if (Lights.Num() > 0) { ++CD.LightCount; } if (USceneComponent* Root = A->GetRootComponent()) { if (Root->Mobility == EComponentMobility::Movable) { ++CD.DynamicActorCount; } } CD.EstimatedMemoryMB += 0.1f; } if (S.bGenerateHLODProxies) { CD.HLODProxyAsset = GenerateHLODProxyForCell(Pair.Key, Pair.Value, S, PackageRoot, Report); } // NOTE: FullCellPackage is left empty here — NonDestructive uses the visibility // backend. DestructiveCommit fills it in CommitConversion(). CD.bHasWarnings = false; // per-cell warning aggregation can be refined later Descriptor->Cells.Add(CD); } Report.GeneratedCells = Descriptor->Cells.Num(); // Empty Z cells (no actors) for reporting completeness. const int32 NumZ = UVerticalPartitionStatics::GetNumZCells(VolumeBounds, S); int32 NonEmptyZ = 0; { TSet SeenZ; for (const FVerticalCellDescriptor& C : Descriptor->Cells) { SeenZ.Add(C.CellId.Z); } NonEmptyZ = SeenZ.Num(); } Report.EmptyCells = FMath::Max(0, NumZ - NonEmptyZ); } FSoftObjectPath UVerticalPartitionBuilder::GenerateHLODProxyForCell(const FVerticalCellId& Cell, const TArray& CellActors, const FVerticalPartitionSettings& S, const FString& PackageRoot, FVerticalPartitionBuildReport& Report) { // Collect STATIC mesh components only — incl. ISM/HISM from Packed Level Actors and from // flattened Level Instance children. Merge auto-expands instances into one object. TArray SMComps; UWorld* World = nullptr; for (AActor* A : CellActors) { if (!A) { continue; } if (!World) { World = A->GetWorld(); } TArray Comps; A->GetComponents(Comps); for (UStaticMeshComponent* C : Comps) { if (C && C->GetStaticMesh() && C->Mobility == EComponentMobility::Static) { SMComps.Add(C); } } if (SMComps.Num() >= S.HLODMaxSourceActorsPerCell) { break; } } if (SMComps.Num() == 0 || !World) { return FSoftObjectPath(); // nothing static to merge; cell just hides when far } const FString MapName = FPackageName::GetShortName(World->GetOutermost()->GetName()); const FString AssetName = FString::Printf(TEXT("HLOD_%s"), *Cell.ToLabel(S.bUseXYSubCells)); const FString PackagePath = PackageRoot / MapName / TEXT("Proxies") / AssetName; UPackage* Package = CreatePackage(*PackagePath); if (!Package) { FVerticalPartitionWarning W; W.Type = EVerticalWarningType::HLODGenerationFailed; W.Cell = Cell; W.Message = FString::Printf(TEXT("Could not create proxy package %s."), *PackagePath); Report.Warnings.Add(W); return FSoftObjectPath(); } const IMeshMergeUtilities& Merge = FModuleManager::Get().LoadModuleChecked("MeshMergeUtilities").GetUtilities(); UStaticMesh* Proxy = nullptr; // ---- SimplifiedProxy: ProxyLOD (simplify + single atlas material). Synchronous when // bAllowAsync=false; needs the ProxyLODPlugin, else it no-ops and we fall back. ---- if (S.HLODMethod == EVerticalHLODMethod::SimplifiedProxy) { FMeshProxySettings ProxySettings; ProxySettings.ScreenSize = FMath::Clamp(S.ProxyScreenSize, 1, 1200); TArray Created; FCreateProxyDelegate Delegate; Delegate.BindLambda([&Created](const FGuid, TArray& Assets) { Created.Append(Assets); }); Merge.CreateProxyMesh(SMComps, ProxySettings, Package, AssetName, FGuid::NewGuid(), Delegate, /*bAllowAsync*/false, 1.0f); for (UObject* O : Created) { if (UStaticMesh* M = Cast(O)) { Proxy = M; break; } } if (!Proxy) { FVerticalPartitionWarning W; W.Type = EVerticalWarningType::HLODGenerationFailed; W.Cell = Cell; W.Message = FString::Printf(TEXT("SimplifiedProxy produced no mesh for cell %s (enable the ProxyLODPlugin). Falling back to MergedMesh."), *Cell.ToString()); Report.Warnings.Add(W); } } // ---- MergedMesh (default, and SimplifiedProxy fallback) ---- if (!Proxy) { TArray Prims; Prims.Reserve(SMComps.Num()); for (UStaticMeshComponent* C : SMComps) { Prims.Add(C); } FMeshMergingSettings MergeSettings; MergeSettings.bMergePhysicsData = false; // Pivot at world origin so the runtime / editor preview spawns it at (0,0,0) over the // source. Merge auto-expands ISM/HISM instances into the single merged mesh. MergeSettings.bPivotPointAtZero = true; TArray OutAssets; FVector OutMergedLocation = FVector::ZeroVector; Merge.MergeComponentsToStaticMesh(Prims, World, MergeSettings, nullptr, Package, AssetName, OutAssets, OutMergedLocation, TNumericLimits::Max(), /*bSilent*/true); for (UObject* O : OutAssets) { if (UStaticMesh* M = Cast(O)) { Proxy = M; break; } } } if (!Proxy) { FVerticalPartitionWarning W; W.Type = EVerticalWarningType::HLODGenerationFailed; W.Cell = Cell; W.Message = FString::Printf(TEXT("HLOD generation produced no mesh for cell %s."), *Cell.ToString()); Report.Warnings.Add(W); return FSoftObjectPath(); } SaveAsset(Proxy); return FSoftObjectPath(Proxy); } // --------------------------------------------------------------------------- // Rebuild / validate // --------------------------------------------------------------------------- bool UVerticalPartitionBuilder::RebuildChangedCells(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport) { // MVP: incremental rebuild == full rebuild (the dependency graph that would let // us touch only changed cells is a V2 item). Logged so the user knows. UE_LOG(LogVerticalPartition, Log, TEXT("[VP] RebuildChangedCells: performing full rebuild (incremental diff is a V2 feature).")); return Build(Volume, OutReport); } bool UVerticalPartitionBuilder::RebuildHLOD(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport) { OutReport.Reset(); if (!Volume) { return false; } UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous(); if (!Desc) { UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] RebuildHLOD: no descriptor. Run Build first.")); return false; } const FVerticalPartitionSettings& S = Volume->Settings; const FString PackageRoot = S.GeneratedPackageRoot; for (FVerticalCellDescriptor& CD : Desc->Cells) { TArray CellActors; for (const FSoftObjectPath& Path : CD.SourceActors) { if (AActor* A = Cast(Path.ResolveObject())) { CellActors.Add(A); } } CD.HLODProxyAsset = GenerateHLODProxyForCell(CD.CellId, CellActors, S, PackageRoot, OutReport); } OutReport.WarningCount = OutReport.Warnings.Num(); Desc->LastReport = OutReport; SaveAsset(Desc); UE_LOG(LogVerticalPartition, Log, TEXT("[VP] RebuildHLOD complete for %d cells."), Desc->Cells.Num()); return true; } FVerticalPartitionBuildReport UVerticalPartitionBuilder::ValidateCells(AVerticalPartitionVolume* Volume) { FVerticalPartitionBuildReport Report = AnalyzeCurrentLevel(Volume); if (!Volume) { return Report; } if (UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous()) { const FBox Bounds = Volume->GetVolumeBounds(); TArray Assignments; FVerticalPartitionBuildReport Tmp; GatherActors(Volume, Bounds, Assignments, Tmp); const int64 CurrentHash = ComputeContentHash(Assignments); if (CurrentHash != Desc->SourceContentHash) { FVerticalPartitionWarning W; W.Type = EVerticalWarningType::DescriptorOutOfDate; W.Message = TEXT("Level content changed since the last Build. Rebuild recommended."); Report.Warnings.Add(W); Report.WarningCount = Report.Warnings.Num(); } } return Report; } bool UVerticalPartitionBuilder::CommitConversion(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport) { // ---- V2 / DestructiveCommit ---- // Intended algorithm (kept as a stub so the plugin compiles & the safe // NonDestructive path ships first, exactly as recommended in the README MVP): // 1. For each cell with actors, UEditorLevelUtils::CreateNewStreamingLevel() // a new sublevel package under //Cells/Cell_Zxxxx.umap. // 2. UEditorLevelUtils::MoveActorsToLevel(CellActors, NewLevel) to bake them out. // 3. Set FVerticalCellDescriptor::FullCellPackage to the new map's soft path so // the runtime switches that cell to the streaming-level backend. // 4. SavePackage() the sublevels + the (now light) persistent level. // This mutates the level destructively, so it must run only after CreateBackup(). UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] CommitConversion (DestructiveCommit) is a V2 feature and is not yet baking sublevels. ") TEXT("The descriptor still works via the NonDestructive visibility backend. See README section 'Destructive Commit'.")); FVerticalPartitionWarning W; W.Type = EVerticalWarningType::DescriptorOutOfDate; W.Message = TEXT("DestructiveCommit not yet implemented; running as NonDestructive."); OutReport.Warnings.Add(W); OutReport.WarningCount = OutReport.Warnings.Num(); return false; } // --------------------------------------------------------------------------- // Backup / clear / report // --------------------------------------------------------------------------- bool UVerticalPartitionBuilder::CreateBackup(UWorld* World, FString& OutBackupPath) { if (!World) { return false; } const FString PackageName = World->GetOutermost()->GetName(); FString SrcFile; if (!FPackageName::TryConvertLongPackageNameToFilename(PackageName, SrcFile, FPackageName::GetMapPackageExtension()) || !IFileManager::Get().FileExists(*SrcFile)) { UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] CreateBackup: source map not found on disk (save the map first).")); return false; } const FString MapShort = FPackageName::GetShortName(PackageName); const FString Stamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S")); OutBackupPath = FPaths::ProjectSavedDir() / TEXT("VerticalPartition") / TEXT("Backups") / FString::Printf(TEXT("%s_%s.umap"), *MapShort, *Stamp); IFileManager::Get().MakeDirectory(*FPaths::GetPath(OutBackupPath), true); const bool bOk = (IFileManager::Get().Copy(*OutBackupPath, *SrcFile) == COPY_OK); UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Backup %s -> %s"), bOk ? TEXT("created") : TEXT("FAILED"), *OutBackupPath); return bOk; } bool UVerticalPartitionBuilder::RestoreFromBackup(UWorld* World) { if (!World) { return false; } const FString PackageName = World->GetOutermost()->GetName(); const FString MapShort = FPackageName::GetShortName(PackageName); const FString BackupDir = FPaths::ProjectSavedDir() / TEXT("VerticalPartition") / TEXT("Backups"); TArray Found; IFileManager::Get().FindFiles(Found, *(BackupDir / (MapShort + TEXT("_*.umap"))), true, false); if (Found.Num() == 0) { UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] RestoreFromBackup: no backups for %s in %s."), *MapShort, *BackupDir); return false; } Found.Sort(); const FString Latest = BackupDir / Found.Last(); FString DstFile; FPackageName::TryConvertLongPackageNameToFilename(PackageName, DstFile, FPackageName::GetMapPackageExtension()); const bool bOk = (IFileManager::Get().Copy(*DstFile, *Latest) == COPY_OK); UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] Restored %s from %s (%s). Reload the map to see changes."), *MapShort, *Latest, bOk ? TEXT("OK") : TEXT("FAILED")); return bOk; } void UVerticalPartitionBuilder::ClearGenerated(AVerticalPartitionVolume* Volume) { if (!Volume) { return; } const FVerticalPartitionSettings& S = Volume->Settings; UWorld* World = Volume->GetWorld(); const FString MapName = World ? FPackageName::GetShortName(World->GetOutermost()->GetName()) : TEXT(""); const FString GeneratedPackageDir = S.GeneratedPackageRoot / MapName; FString DiskDir; if (FPackageName::TryConvertLongPackageNameToFilename(GeneratedPackageDir, DiskDir)) { if (IFileManager::Get().DirectoryExists(*DiskDir)) { IFileManager::Get().DeleteDirectory(*DiskDir, false, true); UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Cleared generated data at %s"), *DiskDir); } } Volume->Descriptor.Reset(); Volume->MarkPackageDirty(); } FString UVerticalPartitionBuilder::DumpReport(const FVerticalPartitionBuildReport& Report, const FString& MapName) { FString Out; Out += FString::Printf(TEXT("Vertical Partition Build Report - %s\n"), *MapName); Out += TEXT("==========================================\n"); Out += FString::Printf(TEXT("Total actors scanned : %d\n"), Report.TotalActorsScanned); Out += FString::Printf(TEXT("Actors inside volume : %d\n"), Report.ActorsInsideVolume); Out += FString::Printf(TEXT("Actors excluded : %d\n"), Report.ActorsExcluded); Out += FString::Printf(TEXT("Generated cells : %d\n"), Report.GeneratedCells); Out += FString::Printf(TEXT("Empty cells : %d\n"), Report.EmptyCells); Out += FString::Printf(TEXT("Largest cell : %s (%d actors)\n"), *Report.LargestCell.ToString(), Report.LargestCellActorCount); Out += FString::Printf(TEXT("Warnings / Errors : %d / %d\n"), Report.WarningCount, Report.ErrorCount); Out += TEXT("\n-- Warnings --\n"); for (const FVerticalPartitionWarning& W : Report.Warnings) { Out += FString::Printf(TEXT(" [%s] %s\n"), *UVerticalPartitionStatics::WarningTypeToString(W.Type), *W.Message); } Out += TEXT("\n-- Errors --\n"); for (const FVerticalPartitionError& E : Report.Errors) { Out += FString::Printf(TEXT(" [%s] %s\n"), *UVerticalPartitionStatics::WarningTypeToString(E.Type), *E.Message); } const FString Path = FPaths::ProjectSavedDir() / TEXT("VerticalPartition") / FString::Printf(TEXT("%s_Report.txt"), *MapName); FFileHelper::SaveStringToFile(Out, *Path); UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Report written to %s\n%s"), *Path, *Out); return Path; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- UVerticalPartitionDescriptor* UVerticalPartitionBuilder::CreateOrLoadDescriptor(AVerticalPartitionVolume* Volume) { UWorld* World = Volume->GetWorld(); if (!World) { return nullptr; } const FString MapName = FPackageName::GetShortName(World->GetOutermost()->GetName()); const FString AssetName = FString::Printf(TEXT("VPD_%s"), *MapName); const FString PackagePath = Volume->Settings.GeneratedPackageRoot / MapName / AssetName; // Re-use the existing descriptor if it is already linked / on disk. if (UVerticalPartitionDescriptor* Existing = Volume->Descriptor.LoadSynchronous()) { return Existing; } if (UVerticalPartitionDescriptor* OnDisk = LoadObject(nullptr, *(PackagePath + TEXT(".") + AssetName))) { return OnDisk; } UPackage* Package = CreatePackage(*PackagePath); if (!Package) { return nullptr; } UVerticalPartitionDescriptor* Desc = NewObject(Package, *AssetName, RF_Public | RF_Standalone); if (Desc) { FAssetRegistryModule::AssetCreated(Desc); } return Desc; } bool UVerticalPartitionBuilder::SaveAsset(UObject* Asset) { if (!Asset) { return false; } UPackage* Package = Asset->GetOutermost(); Package->MarkPackageDirty(); const FString FileName = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension()); FSavePackageArgs Args; Args.TopLevelFlags = RF_Public | RF_Standalone; Args.SaveFlags = SAVE_NoError; Args.Error = GError; const bool bSaved = UPackage::SavePackage(Package, Asset, *FileName, Args); if (!bSaved) { UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] Failed to save %s"), *Package->GetName()); } return bSaved; } int64 UVerticalPartitionBuilder::ComputeContentHash(const TArray& Assignments) { uint32 Hash = 0; for (const FVerticalActorAssignment& A : Assignments) { if (AActor* Actor = A.Actor.Get()) { Hash = HashCombine(Hash, GetTypeHash(Actor->GetFName())); const FVector P = Actor->GetActorLocation(); Hash = HashCombine(Hash, GetTypeHash(FIntVector(FMath::RoundToInt(P.X), FMath::RoundToInt(P.Y), FMath::RoundToInt(P.Z)))); } } Hash = HashCombine(Hash, (uint32)Assignments.Num()); return (int64)Hash; }