Initial commit: Vertical Partition Streaming plugin (UE 5.7)

Bounded vertical route partition / streaming for handcrafted spiral climb maps
(Only Up / Only Climb). Not World Partition, not Level Instances as core arch.

- Z-cell segmentation + optional XY subcells, editor builder + commandlet
- NonDestructive visibility backend + streaming-level backend
- Offline HLOD proxies: MergedMesh / SimplifiedProxy (ProxyLOD)
- Universal Level Instance + Packed Level Actor + HISM support (flatten+merge)
- Vertical-aware runtime streamer (full/preload/HLOD/unload, hysteresis,
  velocity prediction, fall-risk), WP-style debug viz + vp.* console
- Safe-by-default: only static decor streams; sky/lights/gameplay stay persistent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-06-30 17:42:41 +03:00
commit 04ad6d3f25
28 changed files with 4361 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
// Copyright IHY. Vertical Partition Streaming plugin (editor module).
#include "VerticalPartitionCommandlet.h"
#include "VerticalPartitionBuilder.h"
#include "VerticalPartitionVolume.h"
#include "VerticalPartitionLog.h"
#include "EngineUtils.h"
#include "Engine/World.h"
#include "FileHelpers.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
UVerticalPartitionCommandlet::UVerticalPartitionCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 UVerticalPartitionCommandlet::Main(const FString& Params)
{
FString MapPath;
if (!FParse::Value(*Params, TEXT("Map="), MapPath) || MapPath.IsEmpty())
{
UE_LOG(LogVerticalPartition, Error, TEXT("[VP] Commandlet requires -Map=/Game/Maps/MyMap"));
return 1;
}
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Commandlet loading map %s ..."), *MapPath);
UWorld* World = UEditorLoadingAndSavingUtils::LoadMap(MapPath);
if (!World)
{
UE_LOG(LogVerticalPartition, Error, TEXT("[VP] Failed to load map %s"), *MapPath);
return 1;
}
AVerticalPartitionVolume* Volume = nullptr;
for (TActorIterator<AVerticalPartitionVolume> It(World); It; ++It)
{
Volume = *It;
break;
}
if (!Volume)
{
UE_LOG(LogVerticalPartition, Error, TEXT("[VP] No AVerticalPartitionVolume in %s"), *MapPath);
return 1;
}
FVerticalPartitionBuildReport Report;
const bool bOk = UVerticalPartitionBuilder::Build(Volume, Report);
UVerticalPartitionBuilder::DumpReport(Report, FPackageName::GetShortName(MapPath));
if (bOk)
{
TArray<UPackage*> ToSave;
ToSave.Add(World->GetOutermost());
FEditorFileUtils::PromptForCheckoutAndSave(ToSave, /*bCheckDirty*/false, /*bPromptToSave*/false);
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Commandlet build OK: %d cells."), Report.GeneratedCells);
}
return bOk ? 0 : 1;
}

View File

@ -0,0 +1,264 @@
// 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<AStaticMeshActor> 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<UStaticMesh>(CD.HLODProxyAsset.TryLoad());
if (!Mesh) { continue; }
FActorSpawnParameters Params;
Params.ObjectFlags |= RF_Transient;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AStaticMeshActor* Actor = World->SpawnActor<AStaticMeshActor>(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);

View File

@ -0,0 +1,89 @@
// Copyright IHY. Vertical Partition Streaming plugin (editor module).
//
// Editor-only conversion pipeline: scan -> validate -> assign -> generate
// descriptor -> generate HLOD proxies -> (optionally) bake streaming sub-levels.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "VerticalPartitionTypes.h"
#include "VerticalPartitionBuilder.generated.h"
class AVerticalPartitionVolume;
class UVerticalPartitionDescriptor;
class UWorld;
class AActor;
/** One actor's analysed placement, shared between Analyze and Build. */
struct FVerticalActorAssignment
{
TWeakObjectPtr<AActor> Actor;
/** The renderable static-decor leaf actors this unit streams. For a normal actor this
* is just [Actor]; for a Level Instance it is its (recursively flattened) static child
* actors; for a Packed Level Actor it is [Actor] (its baked HISM live on it). */
TArray<TWeakObjectPtr<AActor>> LeafActors;
FVerticalCellId Cell;
FBox Bounds = FBox(ForceInit);
bool bCrossesMultiple = false;
bool bExcluded = false;
bool bLargeKeepPersistent = false;
};
UCLASS()
class UVerticalPartitionBuilder : public UObject
{
GENERATED_BODY()
public:
/** Scan the level, classify every actor, return a report without writing anything. */
static FVerticalPartitionBuildReport AnalyzeCurrentLevel(AVerticalPartitionVolume* Volume);
/** Full build: analyze -> create/refresh descriptor -> generate HLOD proxies ->
* (DestructiveCommit only) bake streaming sub-levels + remove source actors.
* Returns the report; writes the descriptor asset. */
static bool Build(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport);
/** Re-run only cells whose source content hash changed since the last build. */
static bool RebuildChangedCells(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport);
/** (Re)generate HLOD proxy meshes for all cells. */
static bool RebuildHLOD(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport);
/** Re-run analysis against the existing descriptor and flag drift / problems. */
static FVerticalPartitionBuildReport ValidateCells(AVerticalPartitionVolume* Volume);
/** Promote a NonDestructive descriptor to baked streaming sub-levels. */
static bool CommitConversion(AVerticalPartitionVolume* Volume, FVerticalPartitionBuildReport& OutReport);
static bool CreateBackup(UWorld* World, FString& OutBackupPath);
static bool RestoreFromBackup(UWorld* World);
/** Delete generated descriptor / cell maps / proxies under the output root. */
static void ClearGenerated(AVerticalPartitionVolume* Volume);
/** Write the report to Saved/VerticalPartition/<Map>_Report.txt and log it. */
static FString DumpReport(const FVerticalPartitionBuildReport& Report, const FString& MapName);
private:
// ---- pipeline steps ----
static void GatherActors(AVerticalPartitionVolume* Volume, const FBox& VolumeBounds,
TArray<FVerticalActorAssignment>& OutAssignments, FVerticalPartitionBuildReport& Report);
static void ClassifyActorWarnings(const FVerticalActorAssignment& A, const FVerticalPartitionSettings& S,
FVerticalPartitionBuildReport& Report);
static UVerticalPartitionDescriptor* CreateOrLoadDescriptor(AVerticalPartitionVolume* Volume);
static void BuildCells(AVerticalPartitionVolume* Volume, const FBox& VolumeBounds,
const TArray<FVerticalActorAssignment>& Assignments,
UVerticalPartitionDescriptor* Descriptor, FVerticalPartitionBuildReport& Report);
/** Generate one proxy mesh for a cell from its static-mesh actors. Returns the
* asset path or an empty path (with a warning) on failure / unavailable API. */
static FSoftObjectPath GenerateHLODProxyForCell(const FVerticalCellId& Cell,
const TArray<AActor*>& CellActors, const FVerticalPartitionSettings& S,
const FString& PackageRoot, FVerticalPartitionBuildReport& Report);
static int64 ComputeContentHash(const TArray<FVerticalActorAssignment>& Assignments);
static bool SaveAsset(UObject* Asset);
};

View File

@ -0,0 +1,20 @@
// Copyright IHY. Vertical Partition Streaming plugin (editor module).
//
// Offline build entry point:
// UnrealEditor-Cmd.exe Project.uproject -run=VerticalPartition -Map=/Game/Maps/MyMap
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "VerticalPartitionCommandlet.generated.h"
UCLASS()
class UVerticalPartitionCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
UVerticalPartitionCommandlet();
virtual int32 Main(const FString& Params) override;
};

View File

@ -0,0 +1,37 @@
// Copyright IHY. Vertical Partition Streaming plugin (editor module).
using UnrealBuildTool;
public class VerticalPartitionEditor : ModuleRules
{
public VerticalPartitionEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"VerticalPartition",
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"UnrealEd",
"Slate",
"SlateCore",
"InputCore",
"EditorSubsystem",
"ToolMenus",
"Projects",
"AssetRegistry",
"AssetTools",
"MeshMergeUtilities",
"MeshDescription",
"StaticMeshDescription",
"MaterialUtilities",
"MeshReductionInterface",
"LevelEditor",
});
}
}