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>
265 lines
8.9 KiB
C++
265 lines
8.9 KiB
C++
// 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);
|