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:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Unreal build artifacts
|
||||||
|
Binaries/
|
||||||
|
Intermediate/
|
||||||
|
Saved/
|
||||||
|
DerivedDataCache/
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# OS / editor cruft
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
274
README.md
Normal file
274
README.md
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
# Vertical Partition Streaming
|
||||||
|
|
||||||
|
> **This is not an infinite open-world partition system.**
|
||||||
|
> **This is a bounded vertical route partition system for a hand-crafted spiral climbing map** (Only Up / Only Climb style).
|
||||||
|
|
||||||
|
It takes an *already finished* level, scans the actors inside a volume, segments them
|
||||||
|
into vertical **Z cells** (optionally **XY sub-cells**), bakes a **descriptor**, generates
|
||||||
|
**offline HLOD proxies**, and streams the route at runtime with a vertical-aware policy
|
||||||
|
(full / preload / HLOD / unloaded bands, hysteresis, velocity prediction, fall-risk-below),
|
||||||
|
plus World-Partition-style debug visualization.
|
||||||
|
|
||||||
|
It is **not** World Partition, it is **not** Level Instances, and it does **no** runtime
|
||||||
|
mesh merging. Full cells use classic persistent-level + streaming-sublevel loading (or, in
|
||||||
|
the default safe mode, visibility toggling of the existing actors).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. How it understands the task
|
||||||
|
|
||||||
|
| Requirement | How it's met |
|
||||||
|
|---|---|
|
||||||
|
| Bounded vertical route, spiral, handcrafted | `AVerticalPartitionVolume` (a box) defines the playable region; cells are slices of Z. |
|
||||||
|
| Convert an existing map | `UVerticalPartitionBuilder` scans the level in place — nothing is authored from scratch. |
|
||||||
|
| World-Partition *by meaning*, not infinite XY | Z is the primary axis; XY sub-cells are optional refinement. |
|
||||||
|
| Full / HLOD / preload / unload ranges | `FVerticalPartitionSettings` + `UVerticalPartitionStatics::GetDesiredStateForCell`. |
|
||||||
|
| Offline HLOD, no runtime merge | `MeshMergeUtilities` at build time → per-cell proxy `UStaticMesh`. |
|
||||||
|
| Two conversion modes | `NonDestructive` (ships first, safe) and `DestructiveCommit` (V2). |
|
||||||
|
| WP-style debug | `UVerticalPartitionDebugComponent` (3D boxes + overlay) + `vp.*` console. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
EDITOR (VerticalPartitionEditor module) RUNTIME (VerticalPartition module)
|
||||||
|
┌───────────────────────────┐ ┌────────────────────────────────┐
|
||||||
|
│ AVerticalPartitionVolume │ CallInEditor btns │ AVerticalPartitionManager │
|
||||||
|
│ • box bounds + settings │──┐ via bridge │ • finds tracked pawn │
|
||||||
|
│ • Descriptor ref │ │ │ • builds streaming context │
|
||||||
|
└───────────────────────────┘ │ │ • drives subsystem @ interval │
|
||||||
|
▲ ▼ │ • hosts DebugComponent │
|
||||||
|
│ FVerticalPartitionEditorBridge └───────────────┬────────────────┘
|
||||||
|
│ │ │ UpdateStreaming()
|
||||||
|
┌───────────┴────────────┐ ▼ ┌────────────────▼────────────────┐
|
||||||
|
│ UVerticalPartitionBuilder│ Analyze/Build/... │ UVerticalPartitionRuntimeSubsystem│
|
||||||
|
│ • GatherActors │────────────────────────▶│ • per-cell state machine │
|
||||||
|
│ • BuildCells │ writes │ • streaming-level OR visibility │
|
||||||
|
│ • GenerateHLODProxy │ ┌──────────────────┐ │ • proxy actors, async budget │
|
||||||
|
│ • CreateBackup/Commit │────▶│ UVerticalPartition │◀│ • FVerticalRuntimeStats │
|
||||||
|
└──────────────────────────┘ │ Descriptor (asset) │ └───────────────────────────────────┘
|
||||||
|
│ • cells + bounds │
|
||||||
|
UVerticalPartitionCommandlet ────▶│ • settings snapshot│ UVerticalPartitionStatics (pure math,
|
||||||
|
(offline -run=VerticalPartition) │ • last report │ shared by editor + runtime)
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Module split** keeps the heavy editor APIs (mesh merge, level surgery) out of packaged
|
||||||
|
builds. The volume lives in the runtime module so it can be streamed/placed at runtime, and
|
||||||
|
its editor buttons reach the builder through `FVerticalPartitionEditorBridge` (a `TFunction`
|
||||||
|
the editor module installs on startup).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Editor conversion pipeline
|
||||||
|
|
||||||
|
1. Open the finished map.
|
||||||
|
2. Drop an **`AVerticalPartitionVolume`** around the whole playable spiral; scale its box.
|
||||||
|
3. Tune **Settings** on the volume (segmentation, ranges, HLOD, large-actor policy, exclude classes, mode).
|
||||||
|
4. Press the detail-panel buttons (or `vp.*`):
|
||||||
|
- **Analyze** → report only (no writes): counts, cells, largest cell, warnings.
|
||||||
|
- **Build** → backup → scan → classify → `VPD_<Map>` descriptor → HLOD proxies → save → spawn a manager.
|
||||||
|
- **Validate / Preview Chunks / Rebuild HLOD / Rebuild Changed / Clear Generated / Commit / Restore Backup / Dump Report**.
|
||||||
|
5. Press **Play** — the manager registers the descriptor with the subsystem and streaming starts.
|
||||||
|
6. Toggle debug (`vp.Debug 1`).
|
||||||
|
|
||||||
|
**Generated content** lives under `Settings.GeneratedPackageRoot` (default `/Game/VPGenerated/<Map>/`):
|
||||||
|
`VPD_<Map>` (descriptor) and `Proxies/HLOD_Cell_Zxxxx` (proxy meshes). Backups go to
|
||||||
|
`Saved/VerticalPartition/Backups/`.
|
||||||
|
|
||||||
|
### Conversion modes
|
||||||
|
- **NonDestructive (default, ships first):** the persistent level is untouched. The descriptor
|
||||||
|
records each cell's source actors by soft path; at runtime far cells are **hidden**
|
||||||
|
(`SetActorHiddenInGame` + collision/tick off) and their HLOD proxy shown. Zero risk, fully
|
||||||
|
reversible, validates the whole pipeline. *No memory savings* (actors stay resident).
|
||||||
|
- **DestructiveCommit (V2):** actors are baked into per-cell streaming sub-level `.umap`
|
||||||
|
packages and removed from the persistent level → true memory streaming. A backup is taken
|
||||||
|
first. The exact baking steps are documented in `CommitConversion()` and below; the runtime
|
||||||
|
already supports this backend (a cell with a valid `FullCellPackage` switches to streaming).
|
||||||
|
|
||||||
|
> **Recommended MVP: NonDestructive.** It's safe, gives draw-call savings + correct streaming
|
||||||
|
> behavior, and lets you tune ranges before committing to destructive level surgery.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Runtime streaming pipeline
|
||||||
|
|
||||||
|
`AVerticalPartitionManager::Tick` (throttled to `UpdateInterval`) →
|
||||||
|
builds `FVerticalStreamingContext` (player Z cell, velocity Z, camera pitch, teleport flag) →
|
||||||
|
`UVerticalPartitionRuntimeSubsystem::UpdateStreaming`:
|
||||||
|
|
||||||
|
1. **PollStreaming** each cell (refresh state from in-flight async ops).
|
||||||
|
2. **Desired state** per cell = `GetDesiredStateForCell` (or forced / show-only).
|
||||||
|
3. **Budget pass** — nearest `MaxFullCells` stay Full, nearest `MaxVisibleHLODCells` stay HLOD, the rest demote.
|
||||||
|
4. **Apply**, nearest-first, within `MaxAsyncLoadsPerFrame` / `MaxAsyncUnloadsPerFrame`:
|
||||||
|
- `LoadCellFullAsync(visible)` — streaming backend: `SetShouldBeLoaded/Visible`.
|
||||||
|
- `ShowCellHLOD` — spawn/show the proxy `AStaticMeshActor`.
|
||||||
|
- `UnloadCell` — hide actors / unload streaming level / hide proxy.
|
||||||
|
5. **Rebuild stats**, record update time, flag hitches.
|
||||||
|
|
||||||
|
`bSnap` (teleport / checkpoint / first frame) bypasses the per-frame budget for instant convergence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Streaming ranges (all editable per-volume)
|
||||||
|
|
||||||
|
Bands are measured in **cells from the player cell**. Example with `Full=±1`, `Preload above=3`,
|
||||||
|
`HLOD=±... ` and player in **Z10**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Z15 ── Unloaded ───────────────
|
||||||
|
Z14 ─┐
|
||||||
|
Z13 ─┤ HLOD (proxy visible)
|
||||||
|
Z12 ─┘ (+ Preload: full data resident but hidden)
|
||||||
|
Z11 ─┐
|
||||||
|
Z10 ─┤ FULL ← player
|
||||||
|
Z9 ─┘
|
||||||
|
Z8 ─┐ HLOD
|
||||||
|
Z7 ─┘
|
||||||
|
Z6 ── Unloaded ───────────────
|
||||||
|
```
|
||||||
|
|
||||||
|
Key knobs (see `FVerticalPartitionSettings`):
|
||||||
|
`FullLoadCells{Below,Above}`, `PreloadCells{Below,Above}`, `HLODLoadCells{Below,Above}`,
|
||||||
|
`UnloadCells{Below,Above}`, `HysteresisDistanceZ`, `HysteresisCells`, `UpdateInterval`,
|
||||||
|
`MaxAsyncLoads/UnloadsPerFrame`, `MaxVisibleHLODCells`, `MaxFullCells`,
|
||||||
|
`bPrioritizeAbovePlayer`, `bKeepBelowPlayerLoadedOnFallRisk`, `bUseCameraDirectionPriority`,
|
||||||
|
`bUsePlayerVelocityPrediction`, `VelocityPredictionSeconds`.
|
||||||
|
|
||||||
|
`GetDesiredStateForCell` accounts for: bands, **sticky hysteresis** (a loaded cell at the edge
|
||||||
|
stays loaded for `HysteresisCells` extra), **velocity prediction** (shifts the effective player
|
||||||
|
cell), **camera direction**, **fall-risk-below** (keeps below-player cells loaded to the unload
|
||||||
|
edge), forced cells and show-only mode. Teleport is handled by the manager raising `bSnap`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Data structures (`VerticalPartitionTypes.h`)
|
||||||
|
|
||||||
|
`FVerticalCellId{X,Y,Z}` · `EVerticalCellState{Unloaded,LoadingHLOD,HLOD,PreloadingFull,LoadingFull,Full,Unloading,Error}` ·
|
||||||
|
`EVerticalCellDesiredState{Unloaded,HLOD,Full}` · `EVerticalConversionMode{NonDestructive,DestructiveCommit}` ·
|
||||||
|
`ELargeActorPolicy{KeepInPersistentLevel,AssignToDominantCell,DuplicateAsHLODOnly,SplitNotSupportedWarning,ExcludeFromPartition}` ·
|
||||||
|
`FVerticalPartitionSettings` (segmentation + streaming + conversion + HLOD + debug) ·
|
||||||
|
`FVerticalCellDescriptor` · `FVerticalPartitionWarning` / `FVerticalPartitionError` ·
|
||||||
|
`FVerticalPartitionBuildReport` · `FVerticalRuntimeStats`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Classes
|
||||||
|
|
||||||
|
| Class | Module | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `AVerticalPartitionVolume` | Runtime | Bounds + settings + descriptor ref + editor buttons. |
|
||||||
|
| `UVerticalPartitionDeveloperSettings` | Runtime | Project-default settings + global debug (Project Settings → Plugins). |
|
||||||
|
| `UVerticalPartitionStatics` | Runtime | Pure cell math + the desired-state policy. |
|
||||||
|
| `UVerticalPartitionDescriptor` | Runtime | The build artifact (cells, bounds, settings snapshot, report). |
|
||||||
|
| `UVerticalPartitionRuntimeSubsystem` | Runtime | The streaming state machine + stats. |
|
||||||
|
| `AVerticalPartitionManager` | Runtime | Per-level coordinator; drives the subsystem; hosts debug. |
|
||||||
|
| `UVerticalPartitionDebugComponent` | Runtime | 3D cell boxes + on-screen overlay. |
|
||||||
|
| `UVerticalPartitionBuilder` | Editor | Analyze / Build / RebuildHLOD / Validate / Commit / Backup. |
|
||||||
|
| `UVerticalPartitionCommandlet` | Editor | `-run=VerticalPartition -Map=...` offline build. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. HLOD / proxy strategy
|
||||||
|
|
||||||
|
- **Offline only.** Built during Build / Rebuild HLOD via `IMeshMergeUtilities::MergeComponentsToStaticMesh`
|
||||||
|
over each cell's **static** mesh components → one proxy `UStaticMesh` per cell, saved under `Proxies/`.
|
||||||
|
- The runtime spawns one hidden `AStaticMeshActor` per cell from that mesh and toggles its visibility
|
||||||
|
for the HLOD band; Full load hides the proxy, so there's no double-draw.
|
||||||
|
- **Exclusions (never proxied):** dynamic / movable components, physics actors, gameplay actors,
|
||||||
|
moving platforms, lights, audio. Proxy collision is off.
|
||||||
|
- **Graceful degrade:** if a cell has no static geometry or merge fails, the cell still streams —
|
||||||
|
the far representation just hides instead of showing a proxy, and a warning is recorded.
|
||||||
|
|
||||||
|
**Method (`Settings.HLODMethod`):**
|
||||||
|
- **MergedMesh** (default) — `MergeComponentsToStaticMesh`, one merged mesh with original
|
||||||
|
materials; ISM/HISM instances are auto-flattened into the single object.
|
||||||
|
- **SimplifiedProxy** — `CreateProxyMesh` (proxy LOD: simplified geometry + one baked atlas
|
||||||
|
material, the cheapest "true HLOD", same tech as Epic's HLOD *Simplified Mesh*). Requires the
|
||||||
|
**ProxyLODPlugin** enabled; auto-falls-back to MergedMesh with a warning if absent.
|
||||||
|
|
||||||
|
### Level Instances & Packed Level Actors (universal)
|
||||||
|
- **Packed Level Actor** — its baked `ISM/HISM` live as owned components, caught by
|
||||||
|
`GetComponents<UStaticMeshComponent>` and merged (instances flattened to one object).
|
||||||
|
- **Level Instance** — `CollectStreamLeafActors` recursively flattens the LI (runtime API
|
||||||
|
`ULevelInstanceSubsystem::ForEachActorInLevelInstance`, gated on `IsLoaded`) into its static
|
||||||
|
child actors; nested LIs recurse; nested PLAs stay whole. Its static geometry is baked per
|
||||||
|
cell; its static children are hidden/shown at runtime; **lights / gameplay inside the LI stay
|
||||||
|
persistent**. Scan skips LI content (`GetOwningLevelInstance`) so nothing is double-counted.
|
||||||
|
An **unloaded** LI is skipped with a warning — load it in the editor and rebuild.
|
||||||
|
Toggle with `Settings.bStreamLevelInstances` (default on).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Debug / validation
|
||||||
|
|
||||||
|
**Console:** `vp.Debug`, `vp.DebugDraw`, `vp.DebugOverlay` (CVars); commands
|
||||||
|
`vp.Analyze`, `vp.BuildFromSelectedVolume`, `vp.Validate`, `vp.Rebuild`, `vp.RebuildHLOD`,
|
||||||
|
`vp.ForceCellFull/HLOD <Z>`, `vp.UnforceCell <Z>`, `vp.UnforceAll`, `vp.ShowOnlyCell <Z>`,
|
||||||
|
`vp.TeleportToCell <Z>`, `vp.SetFullRange <b> <a>`, `vp.SetHLODRange <b> <a>`,
|
||||||
|
`vp.SetCellHeight <cm>`, `vp.DumpState`, `vp.DumpCells`, `vp.DumpWarnings`, `vp.ProfileStreaming`.
|
||||||
|
|
||||||
|
**3D draw colors:** Green=Full · Blue=HLOD · Yellow=Loading/Transition · Red=Error · Gray=Unloaded · Purple=Forced/locked.
|
||||||
|
**Overlay:** player Z cell, full/HLOD/unloaded counts, loading queue, async ops, est. memory, update ms, hitches.
|
||||||
|
**Editor preview:** *Preview Chunks* draws the Z slices in the editor viewport without entering PIE.
|
||||||
|
|
||||||
|
**Validation warnings** (analyze/build): crosses-multiple-cells, too-large, physics,
|
||||||
|
blueprint-tick, dynamic-mobility, audio/light/nav, replicated/not-stream-safe, cell-too-many-actors,
|
||||||
|
HLOD-generation-failed, package-save-failed, descriptor-out-of-date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Edge cases & how they're handled
|
||||||
|
|
||||||
|
| Case | Handling |
|
||||||
|
|---|---|
|
||||||
|
| Actor crosses Z cells | Assigned to dominant (centre) cell + `ActorCrossesMultipleCells` warning. |
|
||||||
|
| Origin in one cell, bounds in another | `bUseActorBoundsInsteadOfOrigin` picks the rule; assignment uses bounds centre. |
|
||||||
|
| Attached actors split across cells | Grouped per cell in the descriptor; cross-cell attaches are a documented warning to review. |
|
||||||
|
| Blueprint tick / physics / interactable / movable | Flagged; `LargeActorPolicy`/exclude classes keep them in the persistent level. |
|
||||||
|
| SaveGame / replicated actors | `ActorNotStreamSafe` warning — keep in persistent level (NonDestructive leaves them resident). |
|
||||||
|
| Hard ref into another cell | `Dependencies` array on the descriptor (dependency graph is a V2 expansion). |
|
||||||
|
| Decor visible far / silhouette | That's the HLOD proxy's job. |
|
||||||
|
| Light in one cell lighting another | Lights flagged; recommended kept in persistent level or Full cell, not proxied. |
|
||||||
|
| Level Instance / Packed Level Actor | Universal: PLA's HISM and LI's flattened static children baked per-cell HLOD + streamed; LI lights/gameplay stay persistent. `bStreamLevelInstances`. |
|
||||||
|
| HISM / ISM (incl. inside PLA) | Merge auto-expands instances into one merged proxy object. |
|
||||||
|
| Unloaded Level Instance at build | Skipped with a warning — load it in the editor and rebuild. |
|
||||||
|
| Navmesh on a vertical map | Out of scope of streaming; keep navmesh in the persistent level. |
|
||||||
|
| Player falls 10 cells / flies up fast | Velocity prediction + `bKeepBelowPlayerLoadedOnFallRisk` + `bSnap` on big jumps. |
|
||||||
|
| Teleport / checkpoint | Manager detects the jump (>3 cells) → `bSnap` immediate convergence. |
|
||||||
|
| Multiplayer | Streaming is per-world around the local tracked pawn (client-side); replicated actors flagged. Server-authoritative union streaming is V2. |
|
||||||
|
| PIE vs packaged | Editor module is `Type:Editor` (excluded from packaged); runtime is self-contained. |
|
||||||
|
| Package load failure | Cell → `Error` state (red), logged. |
|
||||||
|
| HLOD gen failure | Warning; cell still streams (hides far). |
|
||||||
|
| Source map changed after build | `Validate` flags `DescriptorOutOfDate` via content hash. |
|
||||||
|
| Rebuild only changed cells | `RebuildChangedCells` (currently full rebuild; incremental diff is V2). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. MVP roadmap
|
||||||
|
|
||||||
|
**MVP (implemented):** one volume · Z-only segmentation · `CellHeight` · Analyze · Build
|
||||||
|
(NonDestructive) · descriptor asset · runtime manager + subsystem · full load/unload via
|
||||||
|
visibility · offline HLOD proxy for static-mesh actors · Z-slice debug draw · stats overlay ·
|
||||||
|
warnings for large/dynamic/blueprint/physics actors · full `vp.*` console.
|
||||||
|
|
||||||
|
**V2 (scaffolded / documented):** XY sub-cells (settings + math in place) · DestructiveCommit
|
||||||
|
baking (`CommitConversion` stub + documented steps) · incremental rebuild (dependency graph) ·
|
||||||
|
full HLOD LOD chains · commandlet hardening · multiplayer union streaming · velocity/camera
|
||||||
|
preload tuning · memory profiler integration (Insights) · editor tool *window* (today: detail
|
||||||
|
buttons + console).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Build & enable
|
||||||
|
|
||||||
|
New C++ **modules** were added, so a Live-Coding pass is not enough — regenerate project files
|
||||||
|
and do a full rebuild with the editor closed, then enable the plugin (it's `EnabledByDefault`),
|
||||||
|
or add to `IHY.uproject`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "Name": "VerticalPartition", "Enabled": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
Files live under `Plugins/VerticalPartition/Source/{VerticalPartition,VerticalPartitionEditor}/`.
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionDebugComponent.h"
|
||||||
|
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||||
|
#include "VerticalPartitionStatics.h"
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
#include "DrawDebugHelpers.h"
|
||||||
|
|
||||||
|
static TAutoConsoleVariable<int32> CVarVPDebug(
|
||||||
|
TEXT("vp.Debug"), 0,
|
||||||
|
TEXT("Master Vertical Partition debug toggle (gates DebugDraw + DebugOverlay)."), ECVF_Default);
|
||||||
|
|
||||||
|
static TAutoConsoleVariable<int32> CVarVPDebugDraw(
|
||||||
|
TEXT("vp.DebugDraw"), 1,
|
||||||
|
TEXT("Draw 3D coloured cell boxes / ids / counts (requires vp.Debug 1)."), ECVF_Default);
|
||||||
|
|
||||||
|
static TAutoConsoleVariable<int32> CVarVPDebugOverlay(
|
||||||
|
TEXT("vp.DebugOverlay"), 1,
|
||||||
|
TEXT("Draw the on-screen runtime stats overlay (requires vp.Debug 1)."), ECVF_Default);
|
||||||
|
|
||||||
|
UVerticalPartitionDebugComponent::UVerticalPartitionDebugComponent()
|
||||||
|
{
|
||||||
|
PrimaryComponentTick.bCanEverTick = true;
|
||||||
|
PrimaryComponentTick.bStartWithTickEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
UVerticalPartitionRuntimeSubsystem* UVerticalPartitionDebugComponent::GetSub() const
|
||||||
|
{
|
||||||
|
return UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionDebugComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||||
|
{
|
||||||
|
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||||
|
|
||||||
|
if (CVarVPDebug.GetValueOnGameThread() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UVerticalPartitionRuntimeSubsystem* Sub = GetSub();
|
||||||
|
if (!Sub || !Sub->IsReady())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CVarVPDebugDraw.GetValueOnGameThread() != 0)
|
||||||
|
{
|
||||||
|
DrawDebugCells(Sub);
|
||||||
|
}
|
||||||
|
if (CVarVPDebugOverlay.GetValueOnGameThread() != 0)
|
||||||
|
{
|
||||||
|
DrawOverlay(Sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionDebugComponent::DrawDebugCells(UVerticalPartitionRuntimeSubsystem* Sub) const
|
||||||
|
{
|
||||||
|
#if ENABLE_DRAW_DEBUG
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World || !Sub)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Sub->GetCellRuntimes())
|
||||||
|
{
|
||||||
|
const FVerticalCellRuntime& C = Pair.Value;
|
||||||
|
if (!C.Bounds.IsValid)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purple overrides the state colour when a cell is force-locked.
|
||||||
|
FColor Color = C.bForced
|
||||||
|
? FColor(180, 60, 220)
|
||||||
|
: UVerticalPartitionStatics::GetStateColor(C.State).ToFColor(true);
|
||||||
|
|
||||||
|
const FVector Center = C.Bounds.GetCenter();
|
||||||
|
const FVector Extent = C.Bounds.GetExtent();
|
||||||
|
DrawDebugBox(World, Center, Extent, Color, false, -1.f, 0, 4.f);
|
||||||
|
|
||||||
|
const FString Label = FString::Printf(TEXT("%s [%s] actors:%d"),
|
||||||
|
*C.Id.ToString(),
|
||||||
|
C.bForced ? TEXT("LOCK") : *UEnum::GetDisplayValueAsText(C.State).ToString(),
|
||||||
|
C.ResolvedActors.Num());
|
||||||
|
DrawDebugString(World, Center + FVector(0, 0, Extent.Z * 0.5f), Label, nullptr, Color, 0.f, true);
|
||||||
|
}
|
||||||
|
#endif // ENABLE_DRAW_DEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionDebugComponent::DrawOverlay(UVerticalPartitionRuntimeSubsystem* Sub) const
|
||||||
|
{
|
||||||
|
if (!GEngine || !Sub)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVerticalRuntimeStats& S = Sub->GetStats();
|
||||||
|
const int32 Key = 0x5650; // 'VP'
|
||||||
|
auto Line = [&](int32 Slot, const FColor& Col, const FString& Text)
|
||||||
|
{
|
||||||
|
GEngine->AddOnScreenDebugMessage(Key + Slot, 0.f, Col, Text);
|
||||||
|
};
|
||||||
|
|
||||||
|
Line(0, FColor::Cyan, TEXT("==== Vertical Partition ===="));
|
||||||
|
Line(1, FColor::White, FString::Printf(TEXT("Player Z cell: %d"), S.PlayerCellZ));
|
||||||
|
Line(2, FColor::Green, FString::Printf(TEXT("Full cells: %d"), S.FullCellsLoaded));
|
||||||
|
Line(3, FColor::Blue, FString::Printf(TEXT("HLOD cells: %d"), S.HLODCellsVisible));
|
||||||
|
Line(4, FColor::Silver, FString::Printf(TEXT("Unloaded: %d"), S.CellsUnloaded));
|
||||||
|
Line(5, FColor::Yellow, FString::Printf(TEXT("Loading/async: %d / %d"), S.CellsLoading, S.ActiveAsyncLoads));
|
||||||
|
Line(6, FColor::White, FString::Printf(TEXT("Est. memory: %.1f MB"), S.EstimatedMemoryMB));
|
||||||
|
Line(7, FColor::White, FString::Printf(TEXT("Update time: %.2f ms"), S.LastUpdateTimeMS));
|
||||||
|
Line(8, S.HitchesOverThreshold > 0 ? FColor::Red : FColor::White,
|
||||||
|
FString::Printf(TEXT("Hitches: %d"), S.HitchesOverThreshold));
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionDescriptor.h"
|
||||||
|
|
||||||
|
int32 UVerticalPartitionDescriptor::FindCellIndex(const FVerticalCellId& Id) const
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < Cells.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (Cells[i].CellId == Id)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVerticalCellDescriptor* UVerticalPartitionDescriptor::FindCell(const FVerticalCellId& Id) const
|
||||||
|
{
|
||||||
|
const int32 Index = FindCellIndex(Id);
|
||||||
|
return Cells.IsValidIndex(Index) ? &Cells[Index] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVerticalPartitionDescriptor::HasFullPackages() const
|
||||||
|
{
|
||||||
|
for (const FVerticalCellDescriptor& Cell : Cells)
|
||||||
|
{
|
||||||
|
if (Cell.FullCellPackage.IsValid())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
189
Source/VerticalPartition/Private/VerticalPartitionManager.cpp
Normal file
189
Source/VerticalPartition/Private/VerticalPartitionManager.cpp
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionManager.h"
|
||||||
|
#include "VerticalPartitionDebugComponent.h"
|
||||||
|
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||||
|
#include "VerticalPartitionDescriptor.h"
|
||||||
|
#include "VerticalPartitionVolume.h"
|
||||||
|
#include "VerticalPartitionStatics.h"
|
||||||
|
#include "VerticalPartitionLog.h"
|
||||||
|
#include "EngineUtils.h" // TActorIterator
|
||||||
|
#include "GameFramework/Pawn.h"
|
||||||
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "Camera/PlayerCameraManager.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
AVerticalPartitionManager::AVerticalPartitionManager()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = true;
|
||||||
|
PrimaryActorTick.bStartWithTickEnabled = true;
|
||||||
|
|
||||||
|
DebugComponent = CreateDefaultSubobject<UVerticalPartitionDebugComponent>(TEXT("DebugComponent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVerticalPartitionManager::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
|
||||||
|
// Resolve a volume if none was assigned.
|
||||||
|
if (!Volume && World)
|
||||||
|
{
|
||||||
|
for (TActorIterator<AVerticalPartitionVolume> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
Volume = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the descriptor: explicit -> volume's -> none.
|
||||||
|
UVerticalPartitionDescriptor* Desc = Descriptor.LoadSynchronous();
|
||||||
|
if (!Desc && Volume)
|
||||||
|
{
|
||||||
|
Desc = Volume->Descriptor.LoadSynchronous();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Desc)
|
||||||
|
{
|
||||||
|
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] Manager found no descriptor. Build the partition first. Streaming disabled."));
|
||||||
|
SetActorTickEnabled(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FBox Bounds = Volume ? Volume->GetVolumeBounds() : Desc->VolumeBounds;
|
||||||
|
|
||||||
|
if (UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
Sub->RegisterDescriptor(Desc, Bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
bSnapNextUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVerticalPartitionManager::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
|
{
|
||||||
|
if (UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this))
|
||||||
|
{
|
||||||
|
Sub->UnregisterDescriptor();
|
||||||
|
}
|
||||||
|
Super::EndPlay(EndPlayReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
APawn* AVerticalPartitionManager::ResolveTrackedPawn()
|
||||||
|
{
|
||||||
|
if (TrackedPawn.IsValid())
|
||||||
|
{
|
||||||
|
return TrackedPawn.Get();
|
||||||
|
}
|
||||||
|
if (UWorld* World = GetWorld())
|
||||||
|
{
|
||||||
|
if (APlayerController* PC = World->GetFirstPlayerController())
|
||||||
|
{
|
||||||
|
return PC->GetPawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AVerticalPartitionManager::BuildContext(FVerticalStreamingContext& OutCtx, float DeltaSeconds, bool& bOutTeleport)
|
||||||
|
{
|
||||||
|
bOutTeleport = false;
|
||||||
|
|
||||||
|
APawn* Pawn = ResolveTrackedPawn();
|
||||||
|
UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||||
|
if (!Pawn || !Sub || !Sub->IsReady())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVerticalPartitionSettings& S = Sub->GetSettings();
|
||||||
|
const FBox Bounds = Sub->GetVolumeBounds();
|
||||||
|
const FVector Pos = Pawn->GetActorLocation();
|
||||||
|
|
||||||
|
// Velocity (smoothed) + teleport detection.
|
||||||
|
if (bHavePrevPos && DeltaSeconds > KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
const FVector RawVel = (Pos - PrevTrackedPos) / DeltaSeconds;
|
||||||
|
SmoothedVelocity = FMath::Lerp(SmoothedVelocity, RawVel, 0.5f);
|
||||||
|
|
||||||
|
const float Jump = FMath::Abs(Pos.Z - PrevTrackedPos.Z);
|
||||||
|
if (Jump > S.CellHeight * 3.f)
|
||||||
|
{
|
||||||
|
bOutTeleport = true;
|
||||||
|
SmoothedVelocity = FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PrevTrackedPos = Pos;
|
||||||
|
bHavePrevPos = true;
|
||||||
|
|
||||||
|
OutCtx.PlayerWorldZ = Pos.Z;
|
||||||
|
OutCtx.PlayerVelocityZ = SmoothedVelocity.Z;
|
||||||
|
OutCtx.PlayerCellZ = UVerticalPartitionStatics::GetCellZFromWorldZ(Pos.Z, Bounds.Min.Z, S.CellHeight);
|
||||||
|
OutCtx.bUseXY = S.bUseXYSubCells;
|
||||||
|
if (S.bUseXYSubCells)
|
||||||
|
{
|
||||||
|
OutCtx.PlayerCellX = UVerticalPartitionStatics::GetCellXFromWorldX(Pos.X, Bounds.Min.X, S.XYCellSize);
|
||||||
|
OutCtx.PlayerCellY = UVerticalPartitionStatics::GetCellYFromWorldY(Pos.Y, Bounds.Min.Y, S.XYCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const APlayerController* PC = Cast<APlayerController>(Pawn->GetController()))
|
||||||
|
{
|
||||||
|
if (PC->PlayerCameraManager)
|
||||||
|
{
|
||||||
|
OutCtx.CameraPitchDeg = PC->PlayerCameraManager->GetCameraRotation().Pitch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVerticalPartitionManager::Tick(float DeltaSeconds)
|
||||||
|
{
|
||||||
|
Super::Tick(DeltaSeconds);
|
||||||
|
|
||||||
|
UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||||
|
if (!Sub || !Sub->IsReady())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateAccumulator += DeltaSeconds;
|
||||||
|
const float Interval = FMath::Max(0.01f, Sub->GetSettings().UpdateInterval);
|
||||||
|
if (!bSnapNextUpdate && UpdateAccumulator < Interval)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float Elapsed = UpdateAccumulator;
|
||||||
|
UpdateAccumulator = 0.f;
|
||||||
|
|
||||||
|
FVerticalStreamingContext Ctx;
|
||||||
|
bool bTeleport = false;
|
||||||
|
if (!BuildContext(Ctx, Elapsed, bTeleport))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bSnap = bSnapNextUpdate || bTeleport;
|
||||||
|
bSnapNextUpdate = false;
|
||||||
|
|
||||||
|
Sub->UpdateStreaming(Ctx, Elapsed, bSnap);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVerticalPartitionManager::TeleportToCell(int32 Z)
|
||||||
|
{
|
||||||
|
APawn* Pawn = ResolveTrackedPawn();
|
||||||
|
UVerticalPartitionRuntimeSubsystem* Sub = UVerticalPartitionRuntimeSubsystem::Get(this);
|
||||||
|
if (!Pawn || !Sub || !Sub->IsReady())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVerticalPartitionSettings& S = Sub->GetSettings();
|
||||||
|
const FBox Bounds = Sub->GetVolumeBounds();
|
||||||
|
const FVector Center = Bounds.GetCenter();
|
||||||
|
const float TargetZ = Bounds.Min.Z + (Z + 0.5f) * S.CellHeight;
|
||||||
|
|
||||||
|
Pawn->TeleportTo(FVector(Center.X, Center.Y, TargetZ), Pawn->GetActorRotation());
|
||||||
|
bSnapNextUpdate = true;
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Teleported tracked pawn to cell Z=%d (worldZ=%.0f)"), Z, TargetZ);
|
||||||
|
}
|
||||||
179
Source/VerticalPartition/Private/VerticalPartitionModule.cpp
Normal file
179
Source/VerticalPartition/Private/VerticalPartitionModule.cpp
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// Runtime module impl + the vp.* console command surface. Build/editor commands
|
||||||
|
// route through FVerticalPartitionEditorBridge (editor builds only); runtime
|
||||||
|
// commands talk to the world's UVerticalPartitionRuntimeSubsystem / manager.
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "HAL/IConsoleManager.h"
|
||||||
|
#include "EngineUtils.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "VerticalPartitionLog.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionVolume.h"
|
||||||
|
#include "VerticalPartitionManager.h"
|
||||||
|
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "Selection.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogVerticalPartition);
|
||||||
|
|
||||||
|
namespace VPConsole
|
||||||
|
{
|
||||||
|
static AVerticalPartitionVolume* FindVolume(UWorld* World)
|
||||||
|
{
|
||||||
|
if (!World) { return nullptr; }
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// Prefer an editor-selected volume when available.
|
||||||
|
if (GEditor)
|
||||||
|
{
|
||||||
|
if (USelection* Sel = GEditor->GetSelectedActors())
|
||||||
|
{
|
||||||
|
for (FSelectionIterator It(*Sel); It; ++It)
|
||||||
|
{
|
||||||
|
if (AVerticalPartitionVolume* V = Cast<AVerticalPartitionVolume>(*It))
|
||||||
|
{
|
||||||
|
return V;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
for (TActorIterator<AVerticalPartitionVolume> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
return *It;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static UVerticalPartitionRuntimeSubsystem* GetSub(UWorld* World)
|
||||||
|
{
|
||||||
|
return World ? World->GetSubsystem<UVerticalPartitionRuntimeSubsystem>() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AVerticalPartitionManager* FindManager(UWorld* World)
|
||||||
|
{
|
||||||
|
if (!World) { return nullptr; }
|
||||||
|
for (TActorIterator<AVerticalPartitionManager> It(World); It; ++It)
|
||||||
|
{
|
||||||
|
return *It;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void RouteEditor(UWorld* World, EVerticalPartitionAction Action, const TCHAR* Name)
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
if (AVerticalPartitionVolume* V = FindVolume(World))
|
||||||
|
{
|
||||||
|
FVerticalPartitionEditorBridge::Execute(V, Action);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] %s: no AVerticalPartitionVolume in the level."), Name);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] %s is an editor-only command."), Name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FVerticalPartitionModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void StartupModule() override
|
||||||
|
{
|
||||||
|
using namespace VPConsole;
|
||||||
|
IConsoleManager& CM = IConsoleManager::Get();
|
||||||
|
auto Add = [this, &CM](const TCHAR* Name, const TCHAR* Help, FConsoleCommandWithWorldAndArgsDelegate Del)
|
||||||
|
{
|
||||||
|
Commands.Add(CM.RegisterConsoleCommand(Name, Help, Del, ECVF_Default));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- runtime: reporting ----
|
||||||
|
Add(TEXT("vp.DumpState"), TEXT("Dump runtime streaming stats."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { S->DumpRuntimeStats(); } }));
|
||||||
|
Add(TEXT("vp.DumpCells"), TEXT("Dump every cell's state."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { S->DumpCells(); } }));
|
||||||
|
Add(TEXT("vp.DumpWarnings"), TEXT("Dump build warnings/errors from the descriptor."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { S->DumpWarnings(); } }));
|
||||||
|
Add(TEXT("vp.ProfileStreaming"), TEXT("Log a one-shot streaming profile snapshot."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { S->DumpRuntimeStats(); S->DumpCells(); } }));
|
||||||
|
|
||||||
|
// ---- runtime: forcing / overrides ----
|
||||||
|
Add(TEXT("vp.ForceCellFull"), TEXT("vp.ForceCellFull <Z> - pin a cell to Full."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ForceCellFull(FCString::Atoi(*A[0])); } } }));
|
||||||
|
Add(TEXT("vp.ForceCellHLOD"), TEXT("vp.ForceCellHLOD <Z> - pin a cell to HLOD."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ForceCellHLOD(FCString::Atoi(*A[0])); } } }));
|
||||||
|
Add(TEXT("vp.UnforceCell"), TEXT("vp.UnforceCell <Z> - release a forced cell."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->UnforceCell(FCString::Atoi(*A[0])); } } }));
|
||||||
|
Add(TEXT("vp.UnforceAll"), TEXT("Release all forced cells and ShowOnly mode."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { S->UnforceAll(); } }));
|
||||||
|
Add(TEXT("vp.ShowOnlyCell"), TEXT("vp.ShowOnlyCell <Z> - Full only this cell, unload the rest."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ShowOnlyCell(FCString::Atoi(*A[0])); } } }));
|
||||||
|
Add(TEXT("vp.SetFullRange"), TEXT("vp.SetFullRange <Below> <Above>."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 1) { S->SetFullRange(FCString::Atoi(*A[0]), FCString::Atoi(*A[1])); } } }));
|
||||||
|
Add(TEXT("vp.SetHLODRange"), TEXT("vp.SetHLODRange <Below> <Above>."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 1) { S->SetHLODRange(FCString::Atoi(*A[0]), FCString::Atoi(*A[1])); } } }));
|
||||||
|
Add(TEXT("vp.SetCellHeight"), TEXT("vp.SetCellHeight <cm> (affects prediction only at runtime)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->SetCellHeight(FCString::Atof(*A[0])); } } }));
|
||||||
|
Add(TEXT("vp.TeleportToCell"), TEXT("vp.TeleportToCell <Z> - move the tracked pawn to a cell."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
||||||
|
{ if (auto* M = FindManager(W)) { if (A.Num() > 0) { M->TeleportToCell(FCString::Atoi(*A[0])); } } }));
|
||||||
|
|
||||||
|
// ---- editor/build (routed via the bridge) ----
|
||||||
|
Add(TEXT("vp.Analyze"), TEXT("Analyze the current level (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::Analyze, TEXT("vp.Analyze")); }));
|
||||||
|
Add(TEXT("vp.BuildFromSelectedVolume"), TEXT("Build using the selected/first volume (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::Build, TEXT("vp.BuildFromSelectedVolume")); }));
|
||||||
|
Add(TEXT("vp.Validate"), TEXT("Validate cells against the level (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::Validate, TEXT("vp.Validate")); }));
|
||||||
|
Add(TEXT("vp.Rebuild"), TEXT("Rebuild changed cells (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::RebuildChangedCells, TEXT("vp.Rebuild")); }));
|
||||||
|
Add(TEXT("vp.RebuildHLOD"), TEXT("Rebuild HLOD proxies (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::RebuildHLOD, TEXT("vp.RebuildHLOD")); }));
|
||||||
|
Add(TEXT("vp.PreviewChunks"), TEXT("Draw coloured cell boxes in the editor viewport (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::PreviewChunks, TEXT("vp.PreviewChunks")); }));
|
||||||
|
Add(TEXT("vp.PreviewHLOD"), TEXT("Spawn baked HLOD proxy meshes in the editor viewport (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::SpawnHLODPreview, TEXT("vp.PreviewHLOD")); }));
|
||||||
|
Add(TEXT("vp.PreviewClear"), TEXT("Clear preview boxes + spawned HLOD preview actors (editor)."),
|
||||||
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
||||||
|
{ RouteEditor(W, EVerticalPartitionAction::ClearPreview, TEXT("vp.PreviewClear")); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void ShutdownModule() override
|
||||||
|
{
|
||||||
|
IConsoleManager& CM = IConsoleManager::Get();
|
||||||
|
for (IConsoleCommand* Cmd : Commands)
|
||||||
|
{
|
||||||
|
if (Cmd) { CM.UnregisterConsoleObject(Cmd); }
|
||||||
|
}
|
||||||
|
Commands.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
TArray<IConsoleCommand*> Commands;
|
||||||
|
};
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FVerticalPartitionModule, VerticalPartition);
|
||||||
@ -0,0 +1,632 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionRuntimeSubsystem.h"
|
||||||
|
#include "VerticalPartitionDescriptor.h"
|
||||||
|
#include "VerticalPartitionSettings.h"
|
||||||
|
#include "VerticalPartitionLog.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelStreaming.h"
|
||||||
|
#include "Engine/LevelStreamingDynamic.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Engine/StaticMeshActor.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
|
||||||
|
UVerticalPartitionRuntimeSubsystem* UVerticalPartitionRuntimeSubsystem::Get(const UObject* WorldContextObject)
|
||||||
|
{
|
||||||
|
if (!WorldContextObject)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (const UWorld* World = WorldContextObject->GetWorld())
|
||||||
|
{
|
||||||
|
return World->GetSubsystem<UVerticalPartitionRuntimeSubsystem>();
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::Deinitialize()
|
||||||
|
{
|
||||||
|
UnregisterDescriptor();
|
||||||
|
Super::Deinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::RegisterDescriptor(UVerticalPartitionDescriptor* InDescriptor, const FBox& InVolumeBounds)
|
||||||
|
{
|
||||||
|
UnregisterDescriptor();
|
||||||
|
|
||||||
|
Descriptor = InDescriptor;
|
||||||
|
if (!Descriptor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EffectiveSettings = Descriptor->Settings;
|
||||||
|
VolumeBounds = InVolumeBounds.IsValid ? InVolumeBounds : Descriptor->VolumeBounds;
|
||||||
|
|
||||||
|
Cells.Reset();
|
||||||
|
Cells.Reserve(Descriptor->Cells.Num());
|
||||||
|
|
||||||
|
for (int32 i = 0; i < Descriptor->Cells.Num(); ++i)
|
||||||
|
{
|
||||||
|
const FVerticalCellDescriptor& D = Descriptor->Cells[i];
|
||||||
|
|
||||||
|
FVerticalCellRuntime RT;
|
||||||
|
RT.Id = D.CellId;
|
||||||
|
RT.DescriptorIndex = i;
|
||||||
|
RT.Bounds = D.Bounds;
|
||||||
|
RT.EstimatedMemoryMB = D.EstimatedMemoryMB;
|
||||||
|
RT.bUsesStreamingBackend = D.FullCellPackage.IsValid();
|
||||||
|
|
||||||
|
if (!RT.bUsesStreamingBackend)
|
||||||
|
{
|
||||||
|
// NonDestructive: the actors are already in the persistent level. Resolve
|
||||||
|
// them once and start from "Full" (they are currently visible).
|
||||||
|
RT.ResolvedActors.Reserve(D.SourceActors.Num());
|
||||||
|
for (const FSoftObjectPath& Path : D.SourceActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Cast<AActor>(Path.ResolveObject()))
|
||||||
|
{
|
||||||
|
RT.ResolvedActors.Add(Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RT.State = EVerticalCellState::Full;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RT.State = EVerticalCellState::Unloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cells.Add(RT.Id, MoveTemp(RT));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Registered descriptor '%s' with %d cells (%s backend)."),
|
||||||
|
*Descriptor->GetName(), Cells.Num(), Descriptor->HasFullPackages() ? TEXT("streaming") : TEXT("visibility"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::UnregisterDescriptor()
|
||||||
|
{
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
FVerticalCellRuntime& C = Pair.Value;
|
||||||
|
// Restore persistent actors so the level looks normal after PIE/unregister.
|
||||||
|
if (!C.bUsesStreamingBackend)
|
||||||
|
{
|
||||||
|
SetActorGroupActive(C, true, true, true);
|
||||||
|
}
|
||||||
|
if (C.ProxyActor.IsValid())
|
||||||
|
{
|
||||||
|
C.ProxyActor->Destroy();
|
||||||
|
}
|
||||||
|
if (C.StreamingLevel.IsValid())
|
||||||
|
{
|
||||||
|
C.StreamingLevel->SetShouldBeVisible(false);
|
||||||
|
C.StreamingLevel->SetShouldBeLoaded(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cells.Reset();
|
||||||
|
Descriptor = nullptr;
|
||||||
|
Stats = FVerticalRuntimeStats();
|
||||||
|
bShowOnlyMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main update
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::UpdateStreaming(const FVerticalStreamingContext& Ctx, float /*DeltaSeconds*/, bool bSnap)
|
||||||
|
{
|
||||||
|
if (!Descriptor || Cells.Num() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double T0 = FPlatformTime::Seconds();
|
||||||
|
Stats.PlayerCellZ = Ctx.PlayerCellZ;
|
||||||
|
|
||||||
|
// 1) Desired state for every cell.
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
FVerticalCellRuntime& C = Pair.Value;
|
||||||
|
PollStreaming(C); // refresh State from in-flight async ops first
|
||||||
|
|
||||||
|
if (C.bForced)
|
||||||
|
{
|
||||||
|
C.Desired = C.ForcedState;
|
||||||
|
}
|
||||||
|
else if (bShowOnlyMode)
|
||||||
|
{
|
||||||
|
C.Desired = (C.Id.Z == ShowOnlyZ) ? EVerticalCellDesiredState::Full : EVerticalCellDesiredState::Unloaded;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
C.Desired = UVerticalPartitionStatics::GetDesiredStateForCell(C.Id, Ctx, EffectiveSettings, C.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
C.bPreloadRequested = (C.Desired == EVerticalCellDesiredState::HLOD)
|
||||||
|
&& UVerticalPartitionStatics::ShouldPreloadFull(C.Id, Ctx, EffectiveSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Enforce budgets: keep the nearest MaxFullCells as Full, nearest
|
||||||
|
// MaxVisibleHLODCells as HLOD, demote the rest.
|
||||||
|
{
|
||||||
|
TArray<FVerticalCellRuntime*> Ordered;
|
||||||
|
Ordered.Reserve(Cells.Num());
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
Ordered.Add(&Pair.Value);
|
||||||
|
}
|
||||||
|
const int32 PlayerZ = Ctx.PlayerCellZ;
|
||||||
|
Ordered.Sort([PlayerZ](const FVerticalCellRuntime& A, const FVerticalCellRuntime& B)
|
||||||
|
{
|
||||||
|
return FMath::Abs(A.Id.Z - PlayerZ) < FMath::Abs(B.Id.Z - PlayerZ);
|
||||||
|
});
|
||||||
|
|
||||||
|
int32 FullCount = 0;
|
||||||
|
int32 HlodCount = 0;
|
||||||
|
const int32 MaxFull = FMath::Max(0, EffectiveSettings.MaxFullCells);
|
||||||
|
const int32 MaxHlod = FMath::Max(0, EffectiveSettings.MaxVisibleHLODCells);
|
||||||
|
for (FVerticalCellRuntime* C : Ordered)
|
||||||
|
{
|
||||||
|
if (C->bForced) { continue; } // forced cells ignore budgets
|
||||||
|
if (C->Desired == EVerticalCellDesiredState::Full)
|
||||||
|
{
|
||||||
|
if (FullCount < MaxFull) { ++FullCount; }
|
||||||
|
else { C->Desired = EVerticalCellDesiredState::HLOD; }
|
||||||
|
}
|
||||||
|
if (C->Desired == EVerticalCellDesiredState::HLOD)
|
||||||
|
{
|
||||||
|
if (HlodCount < MaxHlod) { ++HlodCount; }
|
||||||
|
else { C->Desired = EVerticalCellDesiredState::Unloaded; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Apply transitions, nearest first, within the async budget.
|
||||||
|
int32 LoadBudget = bSnap ? MAX_int32 : FMath::Max(1, EffectiveSettings.MaxAsyncLoadsPerFrame);
|
||||||
|
int32 UnloadBudget = bSnap ? MAX_int32 : FMath::Max(1, EffectiveSettings.MaxAsyncUnloadsPerFrame);
|
||||||
|
for (FVerticalCellRuntime* C : Ordered)
|
||||||
|
{
|
||||||
|
ApplyDesiredState(*C, bSnap, LoadBudget, UnloadBudget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RebuildStatsFromCells();
|
||||||
|
Stats.LastUpdateTimeMS = (float)((FPlatformTime::Seconds() - T0) * 1000.0);
|
||||||
|
Stats.LastStreamingTimeMS = Stats.LastUpdateTimeMS;
|
||||||
|
|
||||||
|
if (const UVerticalPartitionDeveloperSettings* Project = GetDefault<UVerticalPartitionDeveloperSettings>())
|
||||||
|
{
|
||||||
|
if (Stats.LastUpdateTimeMS > Project->HitchThresholdMS)
|
||||||
|
{
|
||||||
|
++Stats.HitchesOverThreshold;
|
||||||
|
UE_LOG(LogVerticalPartition, Verbose, TEXT("[VP] Streaming update hitch: %.2f ms"), Stats.LastUpdateTimeMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::ApplyDesiredState(FVerticalCellRuntime& C, bool bSnap, int32& LoadBudget, int32& UnloadBudget)
|
||||||
|
{
|
||||||
|
switch (C.Desired)
|
||||||
|
{
|
||||||
|
case EVerticalCellDesiredState::Full:
|
||||||
|
if (C.bUsesStreamingBackend)
|
||||||
|
{
|
||||||
|
if (C.State != EVerticalCellState::Full)
|
||||||
|
{
|
||||||
|
if (LoadBudget > 0)
|
||||||
|
{
|
||||||
|
LoadCellFullAsync(C, /*bVisible*/true);
|
||||||
|
--LoadBudget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HideProxy(C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetActorGroupActive(C, /*bVisible*/true, /*bCollision*/true, /*bTick*/true);
|
||||||
|
HideProxy(C);
|
||||||
|
C.State = EVerticalCellState::Full;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVerticalCellDesiredState::HLOD:
|
||||||
|
ShowCellHLOD(C);
|
||||||
|
if (C.bUsesStreamingBackend)
|
||||||
|
{
|
||||||
|
if (C.bPreloadRequested)
|
||||||
|
{
|
||||||
|
if (C.State != EVerticalCellState::HLOD && C.State != EVerticalCellState::PreloadingFull && LoadBudget > 0)
|
||||||
|
{
|
||||||
|
LoadCellFullAsync(C, /*bVisible*/false); // resident, hidden
|
||||||
|
--LoadBudget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (C.StreamingLevel.IsValid() && UnloadBudget > 0)
|
||||||
|
{
|
||||||
|
C.StreamingLevel->SetShouldBeVisible(false);
|
||||||
|
C.StreamingLevel->SetShouldBeLoaded(false); // free full data while showing proxy
|
||||||
|
--UnloadBudget;
|
||||||
|
}
|
||||||
|
if (C.State == EVerticalCellState::Full || C.State == EVerticalCellState::Unloaded)
|
||||||
|
{
|
||||||
|
C.State = EVerticalCellState::HLOD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetActorGroupActive(C, /*bVisible*/false, /*bCollision*/false, /*bTick*/false);
|
||||||
|
C.State = EVerticalCellState::HLOD;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EVerticalCellDesiredState::Unloaded:
|
||||||
|
default:
|
||||||
|
if (C.bUsesStreamingBackend)
|
||||||
|
{
|
||||||
|
if (C.State != EVerticalCellState::Unloaded && UnloadBudget > 0)
|
||||||
|
{
|
||||||
|
UnloadCell(C);
|
||||||
|
--UnloadBudget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UnloadCell(C);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::LoadCellFullAsync(FVerticalCellRuntime& C, bool bVisible)
|
||||||
|
{
|
||||||
|
ULevelStreaming* L = EnsureStreamingLevel(C);
|
||||||
|
if (!L)
|
||||||
|
{
|
||||||
|
C.State = EVerticalCellState::Error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
L->SetShouldBeLoaded(true);
|
||||||
|
L->SetShouldBeVisible(bVisible);
|
||||||
|
C.State = bVisible ? EVerticalCellState::LoadingFull : EVerticalCellState::PreloadingFull;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::ShowCellHLOD(FVerticalCellRuntime& C)
|
||||||
|
{
|
||||||
|
if (AStaticMeshActor* Proxy = EnsureProxy(C))
|
||||||
|
{
|
||||||
|
Proxy->SetActorHiddenInGame(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::UnloadCell(FVerticalCellRuntime& C)
|
||||||
|
{
|
||||||
|
if (C.bUsesStreamingBackend)
|
||||||
|
{
|
||||||
|
if (C.StreamingLevel.IsValid())
|
||||||
|
{
|
||||||
|
C.StreamingLevel->SetShouldBeVisible(false);
|
||||||
|
C.StreamingLevel->SetShouldBeLoaded(false);
|
||||||
|
C.State = EVerticalCellState::Unloading;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
C.State = EVerticalCellState::Unloaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetActorGroupActive(C, /*bVisible*/false, /*bCollision*/false, /*bTick*/false);
|
||||||
|
C.State = EVerticalCellState::Unloaded;
|
||||||
|
}
|
||||||
|
HideProxy(C);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::PollStreaming(FVerticalCellRuntime& C)
|
||||||
|
{
|
||||||
|
if (!C.bUsesStreamingBackend || !C.StreamingLevel.IsValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ULevelStreaming* L = C.StreamingLevel.Get();
|
||||||
|
const ELevelStreamingState St = L->GetLevelStreamingState();
|
||||||
|
const bool bVisible = (St == ELevelStreamingState::LoadedVisible);
|
||||||
|
const bool bLoaded = bVisible
|
||||||
|
|| St == ELevelStreamingState::LoadedNotVisible
|
||||||
|
|| St == ELevelStreamingState::MakingVisible
|
||||||
|
|| St == ELevelStreamingState::MakingInvisible;
|
||||||
|
|
||||||
|
if (St == ELevelStreamingState::FailedToLoad)
|
||||||
|
{
|
||||||
|
C.State = EVerticalCellState::Error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (C.Desired == EVerticalCellDesiredState::Full)
|
||||||
|
{
|
||||||
|
if (bVisible) { C.State = EVerticalCellState::Full; HideProxy(C); }
|
||||||
|
else { C.State = EVerticalCellState::LoadingFull; }
|
||||||
|
}
|
||||||
|
else if (C.Desired == EVerticalCellDesiredState::HLOD)
|
||||||
|
{
|
||||||
|
C.State = (C.bPreloadRequested && !bLoaded) ? EVerticalCellState::PreloadingFull : EVerticalCellState::HLOD;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
C.State = bLoaded ? EVerticalCellState::Unloading : EVerticalCellState::Unloaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Backend helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::SetActorGroupActive(FVerticalCellRuntime& C, bool bVisible, bool bCollision, bool bTick)
|
||||||
|
{
|
||||||
|
for (const TWeakObjectPtr<AActor>& Weak : C.ResolvedActors)
|
||||||
|
{
|
||||||
|
if (AActor* Actor = Weak.Get())
|
||||||
|
{
|
||||||
|
Actor->SetActorHiddenInGame(!bVisible);
|
||||||
|
Actor->SetActorEnableCollision(bCollision);
|
||||||
|
Actor->SetActorTickEnabled(bTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AStaticMeshActor* UVerticalPartitionRuntimeSubsystem::EnsureProxy(FVerticalCellRuntime& C)
|
||||||
|
{
|
||||||
|
if (C.ProxyActor.IsValid())
|
||||||
|
{
|
||||||
|
return C.ProxyActor.Get();
|
||||||
|
}
|
||||||
|
if (!Descriptor || !Descriptor->Cells.IsValidIndex(C.DescriptorIndex))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const FVerticalCellDescriptor& D = Descriptor->Cells[C.DescriptorIndex];
|
||||||
|
if (!D.HLODProxyAsset.IsValid())
|
||||||
|
{
|
||||||
|
if (!C.bProxyMissingWarned)
|
||||||
|
{
|
||||||
|
UE_LOG(LogVerticalPartition, Verbose, TEXT("[VP] Cell %s has no HLOD proxy; far cell will simply hide."), *C.Id.ToString());
|
||||||
|
C.bProxyMissingWarned = true;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UStaticMesh* Mesh = Cast<UStaticMesh>(D.HLODProxyAsset.TryLoad());
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!Mesh || !World)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FActorSpawnParameters Params;
|
||||||
|
Params.ObjectFlags |= RF_Transient;
|
||||||
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||||
|
// Proxy meshes are baked with bPivotPointAtZero (world-space verts), so the actor
|
||||||
|
// goes at the world origin and the geometry lands exactly over the source actors.
|
||||||
|
AStaticMeshActor* Actor = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator, Params);
|
||||||
|
if (!Actor)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (UStaticMeshComponent* Comp = Actor->GetStaticMeshComponent())
|
||||||
|
{
|
||||||
|
Comp->SetMobility(EComponentMobility::Movable);
|
||||||
|
Comp->SetStaticMesh(Mesh);
|
||||||
|
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Comp->SetCanEverAffectNavigation(false);
|
||||||
|
}
|
||||||
|
Actor->SetActorHiddenInGame(true);
|
||||||
|
C.ProxyActor = Actor;
|
||||||
|
return Actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::HideProxy(FVerticalCellRuntime& C)
|
||||||
|
{
|
||||||
|
if (C.ProxyActor.IsValid())
|
||||||
|
{
|
||||||
|
C.ProxyActor->SetActorHiddenInGame(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ULevelStreaming* UVerticalPartitionRuntimeSubsystem::EnsureStreamingLevel(FVerticalCellRuntime& C)
|
||||||
|
{
|
||||||
|
if (C.StreamingLevel.IsValid())
|
||||||
|
{
|
||||||
|
return C.StreamingLevel.Get();
|
||||||
|
}
|
||||||
|
UWorld* World = GetWorld();
|
||||||
|
if (!World || !Descriptor || !Descriptor->Cells.IsValidIndex(C.DescriptorIndex))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const FVerticalCellDescriptor& D = Descriptor->Cells[C.DescriptorIndex];
|
||||||
|
const FString PackageName = D.FullCellPackage.GetLongPackageName();
|
||||||
|
if (PackageName.IsEmpty())
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const FName PackageFName(*PackageName);
|
||||||
|
|
||||||
|
for (ULevelStreaming* Existing : World->GetStreamingLevels())
|
||||||
|
{
|
||||||
|
if (Existing && Existing->GetWorldAssetPackageFName() == PackageFName)
|
||||||
|
{
|
||||||
|
C.StreamingLevel = Existing;
|
||||||
|
return Existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ULevelStreamingDynamic* NewStream = NewObject<ULevelStreamingDynamic>(World, ULevelStreamingDynamic::StaticClass(), NAME_None, RF_Transient);
|
||||||
|
NewStream->SetWorldAssetByPackageName(PackageFName);
|
||||||
|
NewStream->SetShouldBeLoaded(false);
|
||||||
|
NewStream->SetShouldBeVisible(false);
|
||||||
|
NewStream->bShouldBlockOnLoad = false;
|
||||||
|
World->AddStreamingLevel(NewStream);
|
||||||
|
C.StreamingLevel = NewStream;
|
||||||
|
return NewStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::RebuildStatsFromCells()
|
||||||
|
{
|
||||||
|
Stats.FullCellsLoaded = 0;
|
||||||
|
Stats.HLODCellsVisible = 0;
|
||||||
|
Stats.CellsUnloaded = 0;
|
||||||
|
Stats.CellsLoading = 0;
|
||||||
|
Stats.ActiveAsyncLoads = 0;
|
||||||
|
Stats.EstimatedMemoryMB = 0.f;
|
||||||
|
|
||||||
|
for (const TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
const FVerticalCellRuntime& C = Pair.Value;
|
||||||
|
switch (C.State)
|
||||||
|
{
|
||||||
|
case EVerticalCellState::Full:
|
||||||
|
++Stats.FullCellsLoaded;
|
||||||
|
Stats.EstimatedMemoryMB += C.EstimatedMemoryMB;
|
||||||
|
break;
|
||||||
|
case EVerticalCellState::HLOD:
|
||||||
|
++Stats.HLODCellsVisible;
|
||||||
|
break;
|
||||||
|
case EVerticalCellState::LoadingFull:
|
||||||
|
case EVerticalCellState::LoadingHLOD:
|
||||||
|
case EVerticalCellState::PreloadingFull:
|
||||||
|
++Stats.CellsLoading;
|
||||||
|
++Stats.ActiveAsyncLoads;
|
||||||
|
Stats.EstimatedMemoryMB += C.EstimatedMemoryMB; // resident while preloading
|
||||||
|
break;
|
||||||
|
case EVerticalCellState::Unloading:
|
||||||
|
++Stats.CellsLoading;
|
||||||
|
break;
|
||||||
|
case EVerticalCellState::Unloaded:
|
||||||
|
default:
|
||||||
|
++Stats.CellsUnloaded;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FVerticalCellRuntime* UVerticalPartitionRuntimeSubsystem::FindCellByZ(int32 Z)
|
||||||
|
{
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
if (Pair.Value.Id.Z == Z)
|
||||||
|
{
|
||||||
|
return &Pair.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Live overrides + debug forcing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::SetFullRange(int32 Below, int32 Above)
|
||||||
|
{
|
||||||
|
EffectiveSettings.FullLoadCellsBelow = FMath::Max(0, Below);
|
||||||
|
EffectiveSettings.FullLoadCellsAbove = FMath::Max(0, Above);
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Full range set to below=%d above=%d"), Below, Above);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::SetHLODRange(int32 Below, int32 Above)
|
||||||
|
{
|
||||||
|
EffectiveSettings.HLODLoadCellsBelow = FMath::Max(0, Below);
|
||||||
|
EffectiveSettings.HLODLoadCellsAbove = FMath::Max(0, Above);
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] HLOD range set to below=%d above=%d"), Below, Above);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::SetCellHeight(float NewHeight)
|
||||||
|
{
|
||||||
|
EffectiveSettings.CellHeight = FMath::Max(1.f, NewHeight);
|
||||||
|
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] CellHeight changed at runtime to %.0f. Note: cell boundaries are baked in the descriptor; this only affects velocity prediction. Rebuild to re-segment."), NewHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::ForceCellFull(int32 Z)
|
||||||
|
{
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
if (Pair.Value.Id.Z == Z) { Pair.Value.bForced = true; Pair.Value.ForcedState = EVerticalCellDesiredState::Full; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::ForceCellHLOD(int32 Z)
|
||||||
|
{
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
if (Pair.Value.Id.Z == Z) { Pair.Value.bForced = true; Pair.Value.ForcedState = EVerticalCellDesiredState::HLOD; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::UnforceCell(int32 Z)
|
||||||
|
{
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
if (Pair.Value.Id.Z == Z) { Pair.Value.bForced = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::UnforceAll()
|
||||||
|
{
|
||||||
|
for (TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
Pair.Value.bForced = false;
|
||||||
|
}
|
||||||
|
bShowOnlyMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::ShowOnlyCell(int32 Z)
|
||||||
|
{
|
||||||
|
bShowOnlyMode = true;
|
||||||
|
ShowOnlyZ = Z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Reporting
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::DumpRuntimeStats() const
|
||||||
|
{
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] ==== Runtime Stats ===="));
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] PlayerCellZ=%d Full=%d HLOD=%d Unloaded=%d Loading=%d Async=%d Mem~%.1fMB Update=%.2fms Hitches=%d"),
|
||||||
|
Stats.PlayerCellZ, Stats.FullCellsLoaded, Stats.HLODCellsVisible, Stats.CellsUnloaded,
|
||||||
|
Stats.CellsLoading, Stats.ActiveAsyncLoads, Stats.EstimatedMemoryMB, Stats.LastUpdateTimeMS, Stats.HitchesOverThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::DumpCells() const
|
||||||
|
{
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] ==== Cells (%d) ===="), Cells.Num());
|
||||||
|
for (const TPair<FVerticalCellId, FVerticalCellRuntime>& Pair : Cells)
|
||||||
|
{
|
||||||
|
const FVerticalCellRuntime& C = Pair.Value;
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] %s state=%d desired=%d actors=%d backend=%s forced=%d"),
|
||||||
|
*C.Id.ToString(), (int32)C.State, (int32)C.Desired, C.ResolvedActors.Num(),
|
||||||
|
C.bUsesStreamingBackend ? TEXT("stream") : TEXT("vis"), C.bForced ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UVerticalPartitionRuntimeSubsystem::DumpWarnings() const
|
||||||
|
{
|
||||||
|
if (!Descriptor)
|
||||||
|
{
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] No descriptor registered."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const FVerticalPartitionBuildReport& R = Descriptor->LastReport;
|
||||||
|
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] ==== Build Warnings (%d) / Errors (%d) ===="), R.Warnings.Num(), R.Errors.Num());
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionSettings.h"
|
||||||
|
|
||||||
|
UVerticalPartitionDeveloperSettings::UVerticalPartitionDeveloperSettings()
|
||||||
|
{
|
||||||
|
// DefaultSettings keeps the struct defaults declared in VerticalPartitionTypes.h.
|
||||||
|
}
|
||||||
|
|
||||||
|
const UVerticalPartitionDeveloperSettings* UVerticalPartitionDeveloperSettings::Get()
|
||||||
|
{
|
||||||
|
return GetDefault<UVerticalPartitionDeveloperSettings>();
|
||||||
|
}
|
||||||
257
Source/VerticalPartition/Private/VerticalPartitionStatics.cpp
Normal file
257
Source/VerticalPartition/Private/VerticalPartitionStatics.cpp
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionStatics.h"
|
||||||
|
|
||||||
|
int32 UVerticalPartitionStatics::GetCellZFromWorldZ(float WorldZ, float VolumeMinZ, float CellHeight)
|
||||||
|
{
|
||||||
|
if (CellHeight <= KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return FMath::FloorToInt((WorldZ - VolumeMinZ) / CellHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVerticalPartitionStatics::GetCellXFromWorldX(float WorldX, float VolumeMinX, float XYCellSize)
|
||||||
|
{
|
||||||
|
if (XYCellSize <= KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return FMath::FloorToInt((WorldX - VolumeMinX) / XYCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVerticalPartitionStatics::GetCellYFromWorldY(float WorldY, float VolumeMinY, float XYCellSize)
|
||||||
|
{
|
||||||
|
if (XYCellSize <= KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return FMath::FloorToInt((WorldY - VolumeMinY) / XYCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UVerticalPartitionStatics::GetNumZCells(const FBox& VolumeBounds, const FVerticalPartitionSettings& S)
|
||||||
|
{
|
||||||
|
if (!VolumeBounds.IsValid || S.CellHeight <= KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const float SizeZ = VolumeBounds.Max.Z - VolumeBounds.Min.Z;
|
||||||
|
return FMath::Max(1, FMath::CeilToInt(SizeZ / S.CellHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
FVerticalCellId UVerticalPartitionStatics::GetCellIdFromPoint(const FVector& Point, const FBox& VolumeBounds, const FVerticalPartitionSettings& S)
|
||||||
|
{
|
||||||
|
FVerticalCellId Id;
|
||||||
|
Id.Z = GetCellZFromWorldZ(Point.Z, VolumeBounds.Min.Z, S.CellHeight);
|
||||||
|
if (S.bUseXYSubCells)
|
||||||
|
{
|
||||||
|
Id.X = GetCellXFromWorldX(Point.X, VolumeBounds.Min.X, S.XYCellSize);
|
||||||
|
Id.Y = GetCellYFromWorldY(Point.Y, VolumeBounds.Min.Y, S.XYCellSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (S.bClampToVolumeBounds && VolumeBounds.IsValid)
|
||||||
|
{
|
||||||
|
const int32 NumZ = GetNumZCells(VolumeBounds, S);
|
||||||
|
Id.Z = FMath::Clamp(Id.Z, 0, NumZ - 1);
|
||||||
|
if (S.bUseXYSubCells && S.XYCellSize > KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
const int32 NumX = FMath::Max(1, FMath::CeilToInt((VolumeBounds.Max.X - VolumeBounds.Min.X) / S.XYCellSize));
|
||||||
|
const int32 NumY = FMath::Max(1, FMath::CeilToInt((VolumeBounds.Max.Y - VolumeBounds.Min.Y) / S.XYCellSize));
|
||||||
|
Id.X = FMath::Clamp(Id.X, 0, NumX - 1);
|
||||||
|
Id.Y = FMath::Clamp(Id.Y, 0, NumY - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVerticalCellId UVerticalPartitionStatics::GetCellIdFromBounds(const FBox& ActorBounds, const FBox& VolumeBounds, const FVerticalPartitionSettings& S, bool& bOutCrossesMultiple)
|
||||||
|
{
|
||||||
|
bOutCrossesMultiple = false;
|
||||||
|
if (!ActorBounds.IsValid)
|
||||||
|
{
|
||||||
|
return FVerticalCellId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dominant cell = the cell containing the bounds centre (deterministic).
|
||||||
|
const FVerticalCellId Id = GetCellIdFromPoint(ActorBounds.GetCenter(), VolumeBounds, S);
|
||||||
|
|
||||||
|
// Does the box span more than one cell on any partitioned axis?
|
||||||
|
const int32 MinZ = GetCellZFromWorldZ(ActorBounds.Min.Z, VolumeBounds.Min.Z, S.CellHeight);
|
||||||
|
const int32 MaxZ = GetCellZFromWorldZ(ActorBounds.Max.Z, VolumeBounds.Min.Z, S.CellHeight);
|
||||||
|
bOutCrossesMultiple = (MinZ != MaxZ);
|
||||||
|
|
||||||
|
if (S.bUseXYSubCells && !bOutCrossesMultiple)
|
||||||
|
{
|
||||||
|
const int32 MinX = GetCellXFromWorldX(ActorBounds.Min.X, VolumeBounds.Min.X, S.XYCellSize);
|
||||||
|
const int32 MaxX = GetCellXFromWorldX(ActorBounds.Max.X, VolumeBounds.Min.X, S.XYCellSize);
|
||||||
|
const int32 MinY = GetCellYFromWorldY(ActorBounds.Min.Y, VolumeBounds.Min.Y, S.XYCellSize);
|
||||||
|
const int32 MaxY = GetCellYFromWorldY(ActorBounds.Max.Y, VolumeBounds.Min.Y, S.XYCellSize);
|
||||||
|
bOutCrossesMultiple = (MinX != MaxX) || (MinY != MaxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
FBox UVerticalPartitionStatics::GetCellBounds(const FVerticalCellId& Cell, const FBox& VolumeBounds, const FVerticalPartitionSettings& S)
|
||||||
|
{
|
||||||
|
FVector Min = VolumeBounds.Min;
|
||||||
|
FVector Max = VolumeBounds.Max;
|
||||||
|
|
||||||
|
Min.Z = VolumeBounds.Min.Z + Cell.Z * S.CellHeight;
|
||||||
|
Max.Z = Min.Z + S.CellHeight;
|
||||||
|
|
||||||
|
if (S.bUseXYSubCells && S.XYCellSize > KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
Min.X = VolumeBounds.Min.X + Cell.X * S.XYCellSize;
|
||||||
|
Max.X = Min.X + S.XYCellSize;
|
||||||
|
Min.Y = VolumeBounds.Min.Y + Cell.Y * S.XYCellSize;
|
||||||
|
Max.Y = Min.Y + S.XYCellSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
FBox Result(Min, Max);
|
||||||
|
if (S.bClampToVolumeBounds && VolumeBounds.IsValid)
|
||||||
|
{
|
||||||
|
Result = Result.Overlap(VolumeBounds);
|
||||||
|
if (!Result.IsValid)
|
||||||
|
{
|
||||||
|
Result = FBox(Min, Max); // degenerate clamp: keep nominal bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute the predicted player Z cell (shared by desired-state + preload).
|
||||||
|
static int32 EffectivePlayerZ(const FVerticalStreamingContext& Ctx, const FVerticalPartitionSettings& S)
|
||||||
|
{
|
||||||
|
int32 Eff = Ctx.PlayerCellZ;
|
||||||
|
if (S.bUsePlayerVelocityPrediction && S.CellHeight > KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
Eff += FMath::RoundToInt((Ctx.PlayerVelocityZ * S.VelocityPredictionSeconds) / S.CellHeight);
|
||||||
|
}
|
||||||
|
return Eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
EVerticalCellDesiredState UVerticalPartitionStatics::GetDesiredStateForCell(
|
||||||
|
const FVerticalCellId& Cell, const FVerticalStreamingContext& Ctx,
|
||||||
|
const FVerticalPartitionSettings& S, EVerticalCellState CurrentState)
|
||||||
|
{
|
||||||
|
const int32 EffPlayerZ = EffectivePlayerZ(Ctx, S);
|
||||||
|
const int32 Dz = Cell.Z - EffPlayerZ; // +above the player
|
||||||
|
|
||||||
|
int32 FullAbove = S.FullLoadCellsAbove;
|
||||||
|
int32 FullBelow = S.FullLoadCellsBelow;
|
||||||
|
int32 HlodAbove = S.HLODLoadCellsAbove;
|
||||||
|
int32 HlodBelow = S.HLODLoadCellsBelow;
|
||||||
|
|
||||||
|
// Bias the reach upward: in a climbing map the route is above you.
|
||||||
|
if (S.bPrioritizeAbovePlayer)
|
||||||
|
{
|
||||||
|
HlodAbove += 1;
|
||||||
|
}
|
||||||
|
// Look up -> extend upward; look down -> extend downward.
|
||||||
|
if (S.bUseCameraDirectionPriority)
|
||||||
|
{
|
||||||
|
if (Ctx.CameraPitchDeg > 15.f) { FullAbove += 1; HlodAbove += 1; }
|
||||||
|
else if (Ctx.CameraPitchDeg < -15.f) { HlodBelow += 1; }
|
||||||
|
}
|
||||||
|
// You can always fall back down: keep the cells below loaded out to the unload edge.
|
||||||
|
if (S.bKeepBelowPlayerLoadedOnFallRisk)
|
||||||
|
{
|
||||||
|
HlodBelow = FMath::Max(HlodBelow, S.UnloadCellsBelow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Hys = FMath::Max(0, S.HysteresisCells);
|
||||||
|
const bool bCurFullish = (CurrentState == EVerticalCellState::Full
|
||||||
|
|| CurrentState == EVerticalCellState::LoadingFull
|
||||||
|
|| CurrentState == EVerticalCellState::PreloadingFull);
|
||||||
|
const bool bCurHlodish = (CurrentState == EVerticalCellState::HLOD
|
||||||
|
|| CurrentState == EVerticalCellState::LoadingHLOD);
|
||||||
|
|
||||||
|
EVerticalCellDesiredState Result;
|
||||||
|
if (Dz >= -FullBelow && Dz <= FullAbove)
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::Full;
|
||||||
|
}
|
||||||
|
else if (bCurFullish && Dz >= -(FullBelow + Hys) && Dz <= (FullAbove + Hys))
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::Full; // sticky: don't drop a loaded cell at the edge
|
||||||
|
}
|
||||||
|
else if (Dz >= -HlodBelow && Dz <= HlodAbove)
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::HLOD;
|
||||||
|
}
|
||||||
|
else if (bCurHlodish && Dz >= -(HlodBelow + Hys) && Dz <= (HlodAbove + Hys))
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::HLOD;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::Unloaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XY refinement (optional): far columns never go full and drop out past 2 rings.
|
||||||
|
if (Ctx.bUseXY)
|
||||||
|
{
|
||||||
|
const int32 XYDist = FMath::Max(FMath::Abs(Cell.X - Ctx.PlayerCellX), FMath::Abs(Cell.Y - Ctx.PlayerCellY));
|
||||||
|
if (XYDist > 2)
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::Unloaded;
|
||||||
|
}
|
||||||
|
else if (XYDist > 1 && Result == EVerticalCellDesiredState::Full)
|
||||||
|
{
|
||||||
|
Result = EVerticalCellDesiredState::HLOD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UVerticalPartitionStatics::ShouldPreloadFull(const FVerticalCellId& Cell, const FVerticalStreamingContext& Ctx, const FVerticalPartitionSettings& S)
|
||||||
|
{
|
||||||
|
const int32 EffPlayerZ = EffectivePlayerZ(Ctx, S);
|
||||||
|
const int32 Dz = Cell.Z - EffPlayerZ;
|
||||||
|
const bool bAbovePreload = (Dz > S.FullLoadCellsAbove && Dz <= S.PreloadCellsAbove);
|
||||||
|
const bool bBelowPreload = (Dz < -S.FullLoadCellsBelow && Dz >= -S.PreloadCellsBelow);
|
||||||
|
return bAbovePreload || bBelowPreload;
|
||||||
|
}
|
||||||
|
|
||||||
|
FLinearColor UVerticalPartitionStatics::GetStateColor(EVerticalCellState State)
|
||||||
|
{
|
||||||
|
switch (State)
|
||||||
|
{
|
||||||
|
case EVerticalCellState::Full: return FLinearColor(0.10f, 0.85f, 0.15f); // green
|
||||||
|
case EVerticalCellState::HLOD: return FLinearColor(0.15f, 0.45f, 0.95f); // blue
|
||||||
|
case EVerticalCellState::LoadingHLOD:
|
||||||
|
case EVerticalCellState::LoadingFull:
|
||||||
|
case EVerticalCellState::PreloadingFull:
|
||||||
|
case EVerticalCellState::Unloading: return FLinearColor(0.95f, 0.85f, 0.10f); // yellow
|
||||||
|
case EVerticalCellState::Error: return FLinearColor(0.95f, 0.10f, 0.10f); // red
|
||||||
|
case EVerticalCellState::Unloaded:
|
||||||
|
default: return FLinearColor(0.35f, 0.35f, 0.35f); // gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UVerticalPartitionStatics::WarningTypeToString(EVerticalWarningType Type)
|
||||||
|
{
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case EVerticalWarningType::ActorCrossesMultipleCells: return TEXT("Actor crosses multiple cells");
|
||||||
|
case EVerticalWarningType::ActorTooLarge: return TEXT("Actor too large");
|
||||||
|
case EVerticalWarningType::ActorSimulatesPhysics: return TEXT("Actor simulates physics");
|
||||||
|
case EVerticalWarningType::ActorHasBlueprintTick: return TEXT("Actor has Blueprint tick");
|
||||||
|
case EVerticalWarningType::ActorDynamicMobility: return TEXT("Actor has dynamic mobility");
|
||||||
|
case EVerticalWarningType::ActorHasExternalReferences:return TEXT("Actor has external references");
|
||||||
|
case EVerticalWarningType::ActorAttachedAcrossCells: return TEXT("Attached children in another cell");
|
||||||
|
case EVerticalWarningType::ActorDependsOnOtherCell: return TEXT("Depends on actor in another cell");
|
||||||
|
case EVerticalWarningType::ActorNeedsManualReview: return TEXT("Gameplay tag requires manual review");
|
||||||
|
case EVerticalWarningType::ActorHasAudioLightNav: return TEXT("Contains audio/light/nav components");
|
||||||
|
case EVerticalWarningType::ActorNotStreamSafe: return TEXT("Actor not safe for streaming");
|
||||||
|
case EVerticalWarningType::ActorHasUnsavedChanges: return TEXT("Actor has unsaved changes");
|
||||||
|
case EVerticalWarningType::CellTooManyActors: return TEXT("Cell has too many actors");
|
||||||
|
case EVerticalWarningType::CellTooManyDrawCalls: return TEXT("Cell has too many draw calls");
|
||||||
|
case EVerticalWarningType::HLODGenerationFailed: return TEXT("HLOD generation failed");
|
||||||
|
case EVerticalWarningType::PackageSaveFailed: return TEXT("Package save failed");
|
||||||
|
case EVerticalWarningType::NoVolume: return TEXT("No partition volume");
|
||||||
|
case EVerticalWarningType::DescriptorOutOfDate: return TEXT("Descriptor out of date");
|
||||||
|
default: return TEXT("None");
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Source/VerticalPartition/Private/VerticalPartitionVolume.cpp
Normal file
71
Source/VerticalPartition/Private/VerticalPartitionVolume.cpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#include "VerticalPartitionVolume.h"
|
||||||
|
#include "VerticalPartitionSettings.h"
|
||||||
|
#include "VerticalPartitionLog.h"
|
||||||
|
#include "Components/BoxComponent.h"
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
TFunction<void(AVerticalPartitionVolume*, EVerticalPartitionAction)> FVerticalPartitionEditorBridge::Handler;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
AVerticalPartitionVolume::AVerticalPartitionVolume()
|
||||||
|
{
|
||||||
|
PrimaryActorTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Bounds"));
|
||||||
|
SetRootComponent(Box);
|
||||||
|
Box->SetBoxExtent(FVector(5000.f, 5000.f, 25000.f));
|
||||||
|
Box->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
Box->SetCollisionProfileName(TEXT("NoCollision"));
|
||||||
|
Box->SetGenerateOverlapEvents(false);
|
||||||
|
Box->ShapeColor = FColor(80, 200, 255);
|
||||||
|
Box->bDrawOnlyIfSelected = false;
|
||||||
|
Box->SetHiddenInGame(true);
|
||||||
|
|
||||||
|
// Seed per-volume settings from project defaults.
|
||||||
|
if (const UVerticalPartitionDeveloperSettings* Project = UVerticalPartitionDeveloperSettings::Get())
|
||||||
|
{
|
||||||
|
Settings = Project->DefaultSettings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FBox AVerticalPartitionVolume::GetVolumeBounds() const
|
||||||
|
{
|
||||||
|
if (!Box)
|
||||||
|
{
|
||||||
|
return FBox(ForceInit);
|
||||||
|
}
|
||||||
|
const FVector Extent = Box->GetScaledBoxExtent();
|
||||||
|
const FVector Origin = Box->GetComponentLocation();
|
||||||
|
return FBox(Origin - Extent, Origin + Extent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVerticalPartitionVolume::OnConstruction(const FTransform& Transform)
|
||||||
|
{
|
||||||
|
Super::OnConstruction(Transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
void AVerticalPartitionVolume::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
|
||||||
|
{
|
||||||
|
Super::PostEditChangeProperty(PropertyChangedEvent);
|
||||||
|
// Keep preview drawing fresh when ranges/segmentation change.
|
||||||
|
if (Box)
|
||||||
|
{
|
||||||
|
Box->MarkRenderStateDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVerticalPartitionVolume::AnalyzeCurrentLevel() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Analyze); }
|
||||||
|
void AVerticalPartitionVolume::BuildVerticalPartition(){ FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Build); }
|
||||||
|
void AVerticalPartitionVolume::RebuildChangedCells() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::RebuildChangedCells); }
|
||||||
|
void AVerticalPartitionVolume::RebuildHLOD() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::RebuildHLOD); }
|
||||||
|
void AVerticalPartitionVolume::ValidateCells() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Validate); }
|
||||||
|
void AVerticalPartitionVolume::PreviewChunks() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::PreviewChunks); }
|
||||||
|
void AVerticalPartitionVolume::SpawnHLODPreview() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::SpawnHLODPreview); }
|
||||||
|
void AVerticalPartitionVolume::ClearPreview() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::ClearPreview); }
|
||||||
|
void AVerticalPartitionVolume::CommitConversion() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::Commit); }
|
||||||
|
void AVerticalPartitionVolume::RestoreFromBackup() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::RestoreBackup); }
|
||||||
|
void AVerticalPartitionVolume::ClearGeneratedData() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::ClearGenerated); }
|
||||||
|
void AVerticalPartitionVolume::DumpReport() { FVerticalPartitionEditorBridge::Execute(this, EVerticalPartitionAction::DumpReport); }
|
||||||
|
#endif
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// World-Partition-style debug: coloured cell boxes, ids, actor counts, problem
|
||||||
|
// actor highlights, and an on-screen runtime overlay. Driven by the vp.Debug /
|
||||||
|
// vp.DebugDraw / vp.DebugOverlay console variables (and the volume's debug flags).
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "VerticalPartitionDebugComponent.generated.h"
|
||||||
|
|
||||||
|
class UVerticalPartitionRuntimeSubsystem;
|
||||||
|
|
||||||
|
UCLASS(ClassGroup=(VerticalPartition), meta=(BlueprintSpawnableComponent))
|
||||||
|
class VERTICALPARTITION_API UVerticalPartitionDebugComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UVerticalPartitionDebugComponent();
|
||||||
|
|
||||||
|
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||||
|
|
||||||
|
/** 3D coloured cell boxes + ids + counts (the spec's DrawDebugCells). */
|
||||||
|
void DrawDebugCells(UVerticalPartitionRuntimeSubsystem* Sub) const;
|
||||||
|
/** On-screen text block of FVerticalRuntimeStats. */
|
||||||
|
void DrawOverlay(UVerticalPartitionRuntimeSubsystem* Sub) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UVerticalPartitionRuntimeSubsystem* GetSub() const;
|
||||||
|
};
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// The build artifact: a data asset describing every generated cell plus a snapshot
|
||||||
|
// of the settings and volume bounds the build was run with. The runtime streamer
|
||||||
|
// consumes this; the editor builder produces it.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DataAsset.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionDescriptor.generated.h"
|
||||||
|
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class VERTICALPARTITION_API UVerticalPartitionDescriptor : public UDataAsset
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Map this descriptor was built from (for rebuild / out-of-date checks). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FSoftObjectPath SourceLevel;
|
||||||
|
|
||||||
|
/** Settings snapshot used at build time (runtime ranges may be overridden live). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
FVerticalPartitionSettings Settings;
|
||||||
|
|
||||||
|
/** World bounds of the partition volume at build time. */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FBox VolumeBounds = FBox(ForceInit);
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
EVerticalConversionMode BuiltMode = EVerticalConversionMode::NonDestructive;
|
||||||
|
|
||||||
|
/** Content hash of the source actors at build time (for incremental rebuild). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int64 SourceContentHash = 0;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
TArray<FVerticalCellDescriptor> Cells;
|
||||||
|
|
||||||
|
/** Last build report (kept for the UI / Dump Report). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FVerticalPartitionBuildReport LastReport;
|
||||||
|
|
||||||
|
/** Find a cell descriptor by id. Returns nullptr if absent. */
|
||||||
|
const FVerticalCellDescriptor* FindCell(const FVerticalCellId& Id) const;
|
||||||
|
int32 FindCellIndex(const FVerticalCellId& Id) const;
|
||||||
|
|
||||||
|
bool HasFullPackages() const;
|
||||||
|
};
|
||||||
6
Source/VerticalPartition/Public/VerticalPartitionLog.h
Normal file
6
Source/VerticalPartition/Public/VerticalPartitionLog.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
|
||||||
|
VERTICALPARTITION_API DECLARE_LOG_CATEGORY_EXTERN(LogVerticalPartition, Log, All);
|
||||||
62
Source/VerticalPartition/Public/VerticalPartitionManager.h
Normal file
62
Source/VerticalPartition/Public/VerticalPartitionManager.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// In-world coordinator. One per streamed level (auto-spawned by the volume at
|
||||||
|
// BeginPlay, or placed manually). Determines the tracked player's current cell,
|
||||||
|
// drives the runtime subsystem at the configured interval, and hosts the debug
|
||||||
|
// component. Keep this lightweight: the heavy lifting is in the subsystem.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionManager.generated.h"
|
||||||
|
|
||||||
|
class UVerticalPartitionDescriptor;
|
||||||
|
class UVerticalPartitionDebugComponent;
|
||||||
|
class AVerticalPartitionVolume;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class VERTICALPARTITION_API AVerticalPartitionManager : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AVerticalPartitionManager();
|
||||||
|
|
||||||
|
/** Descriptor to stream. If unset, the manager searches the level for a volume. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
TSoftObjectPtr<UVerticalPartitionDescriptor> Descriptor;
|
||||||
|
|
||||||
|
/** Optional explicit volume (otherwise the first one found is used). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
TObjectPtr<AVerticalPartitionVolume> Volume;
|
||||||
|
|
||||||
|
/** Pawn whose position drives streaming. Defaults to local player pawn each tick. */
|
||||||
|
UPROPERTY(BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
TWeakObjectPtr<APawn> TrackedPawn;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, Category="VerticalPartition")
|
||||||
|
TObjectPtr<UVerticalPartitionDebugComponent> DebugComponent;
|
||||||
|
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||||
|
virtual void Tick(float DeltaSeconds) override;
|
||||||
|
|
||||||
|
/** Snap streaming to the tracked pawn immediately (checkpoint / teleport). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category="VerticalPartition")
|
||||||
|
void RequestSnap() { bSnapNextUpdate = true; }
|
||||||
|
|
||||||
|
/** Teleport the tracked pawn to the centre of a Z cell (debug). */
|
||||||
|
UFUNCTION(BlueprintCallable, Category="VerticalPartition")
|
||||||
|
void TeleportToCell(int32 Z);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float UpdateAccumulator = 0.f;
|
||||||
|
bool bSnapNextUpdate = true; // snap on the first update
|
||||||
|
bool bHavePrevPos = false;
|
||||||
|
FVector PrevTrackedPos = FVector::ZeroVector;
|
||||||
|
FVector SmoothedVelocity = FVector::ZeroVector;
|
||||||
|
|
||||||
|
APawn* ResolveTrackedPawn();
|
||||||
|
bool BuildContext(struct FVerticalStreamingContext& OutCtx, float DeltaSeconds, bool& bOutTeleport);
|
||||||
|
};
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// The runtime streaming engine. World-scoped, holds the per-cell state machine,
|
||||||
|
// the async load queue, the proxy actors and the live statistics. Driven each
|
||||||
|
// update tick by AVerticalPartitionManager (subsystems are not ticked directly).
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Subsystems/WorldSubsystem.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionStatics.h"
|
||||||
|
#include "VerticalPartitionRuntimeSubsystem.generated.h"
|
||||||
|
|
||||||
|
class UVerticalPartitionDescriptor;
|
||||||
|
class ULevelStreaming;
|
||||||
|
class AStaticMeshActor;
|
||||||
|
|
||||||
|
/** Per-cell live runtime record. Plain struct (C++-only, same-module access). */
|
||||||
|
struct FVerticalCellRuntime
|
||||||
|
{
|
||||||
|
FVerticalCellId Id;
|
||||||
|
int32 DescriptorIndex = INDEX_NONE;
|
||||||
|
FBox Bounds = FBox(ForceInit);
|
||||||
|
|
||||||
|
EVerticalCellState State = EVerticalCellState::Unloaded;
|
||||||
|
EVerticalCellDesiredState Desired = EVerticalCellDesiredState::Unloaded;
|
||||||
|
|
||||||
|
/** Backend B (DestructiveCommit): the streaming sub-level for the full actors. */
|
||||||
|
TWeakObjectPtr<ULevelStreaming> StreamingLevel;
|
||||||
|
/** Backend A (NonDestructive): the original persistent-level actors of this cell. */
|
||||||
|
TArray<TWeakObjectPtr<AActor>> ResolvedActors;
|
||||||
|
/** Shown while the cell is HLOD. */
|
||||||
|
TWeakObjectPtr<AStaticMeshActor> ProxyActor;
|
||||||
|
|
||||||
|
bool bUsesStreamingBackend = false; // true => backend B
|
||||||
|
bool bPreloadRequested = false;
|
||||||
|
bool bProxyMissingWarned = false;
|
||||||
|
|
||||||
|
/** Debug lock: when set, Desired is pinned to ForcedState. */
|
||||||
|
bool bForced = false;
|
||||||
|
EVerticalCellDesiredState ForcedState = EVerticalCellDesiredState::Full;
|
||||||
|
|
||||||
|
float EstimatedMemoryMB = 0.f;
|
||||||
|
double LastChangeTime = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class VERTICALPARTITION_API UVerticalPartitionRuntimeSubsystem : public UWorldSubsystem
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
static UVerticalPartitionRuntimeSubsystem* Get(const UObject* WorldContextObject);
|
||||||
|
|
||||||
|
virtual void Deinitialize() override;
|
||||||
|
|
||||||
|
// ---- lifecycle (called by the manager) ----
|
||||||
|
void RegisterDescriptor(UVerticalPartitionDescriptor* InDescriptor, const FBox& InVolumeBounds);
|
||||||
|
void UnregisterDescriptor();
|
||||||
|
bool IsReady() const { return Descriptor != nullptr; }
|
||||||
|
|
||||||
|
/** Main entry point. Recomputes desired states and steps the machine within the
|
||||||
|
* per-update async budget. bSnap forces immediate convergence (teleport/checkpoint). */
|
||||||
|
void UpdateStreaming(const FVerticalStreamingContext& Ctx, float DeltaSeconds, bool bSnap = false);
|
||||||
|
|
||||||
|
// ---- live range overrides (console) ----
|
||||||
|
FVerticalPartitionSettings& GetMutableSettings() { return EffectiveSettings; }
|
||||||
|
const FVerticalPartitionSettings& GetSettings() const { return EffectiveSettings; }
|
||||||
|
void SetFullRange(int32 Below, int32 Above);
|
||||||
|
void SetHLODRange(int32 Below, int32 Above);
|
||||||
|
void SetCellHeight(float NewHeight);
|
||||||
|
|
||||||
|
// ---- debug forcing ----
|
||||||
|
void ForceCellFull(int32 Z);
|
||||||
|
void ForceCellHLOD(int32 Z);
|
||||||
|
void UnforceCell(int32 Z);
|
||||||
|
void UnforceAll();
|
||||||
|
void ShowOnlyCell(int32 Z); // force one Full, everything else Unloaded
|
||||||
|
|
||||||
|
// ---- introspection ----
|
||||||
|
const FVerticalRuntimeStats& GetStats() const { return Stats; }
|
||||||
|
const TMap<FVerticalCellId, FVerticalCellRuntime>& GetCellRuntimes() const { return Cells; }
|
||||||
|
const FBox& GetVolumeBounds() const { return VolumeBounds; }
|
||||||
|
UVerticalPartitionDescriptor* GetDescriptor() const { return Descriptor; }
|
||||||
|
int32 GetPlayerCellZ() const { return Stats.PlayerCellZ; }
|
||||||
|
|
||||||
|
// ---- reporting (console) ----
|
||||||
|
void DumpRuntimeStats() const;
|
||||||
|
void DumpCells() const;
|
||||||
|
void DumpWarnings() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY(Transient)
|
||||||
|
TObjectPtr<UVerticalPartitionDescriptor> Descriptor = nullptr;
|
||||||
|
|
||||||
|
FVerticalPartitionSettings EffectiveSettings;
|
||||||
|
FBox VolumeBounds = FBox(ForceInit);
|
||||||
|
|
||||||
|
TMap<FVerticalCellId, FVerticalCellRuntime> Cells;
|
||||||
|
FVerticalRuntimeStats Stats;
|
||||||
|
|
||||||
|
bool bShowOnlyMode = false;
|
||||||
|
int32 ShowOnlyZ = 0;
|
||||||
|
|
||||||
|
// ---- state-machine helpers (the spec's key runtime functions) ----
|
||||||
|
void ApplyDesiredState(FVerticalCellRuntime& Cell, bool bSnap, int32& LoadBudget, int32& UnloadBudget);
|
||||||
|
void LoadCellFullAsync(FVerticalCellRuntime& Cell, bool bVisible);
|
||||||
|
void ShowCellHLOD(FVerticalCellRuntime& Cell);
|
||||||
|
void UnloadCell(FVerticalCellRuntime& Cell);
|
||||||
|
void PollStreaming(FVerticalCellRuntime& Cell);
|
||||||
|
|
||||||
|
void SetActorGroupActive(FVerticalCellRuntime& Cell, bool bVisible, bool bCollision, bool bTick);
|
||||||
|
AStaticMeshActor* EnsureProxy(FVerticalCellRuntime& Cell);
|
||||||
|
void HideProxy(FVerticalCellRuntime& Cell);
|
||||||
|
ULevelStreaming* EnsureStreamingLevel(FVerticalCellRuntime& Cell);
|
||||||
|
|
||||||
|
void RebuildStatsFromCells();
|
||||||
|
FVerticalCellRuntime* FindCellByZ(int32 Z); // first cell at this Z (XY=0 path)
|
||||||
|
};
|
||||||
36
Source/VerticalPartition/Public/VerticalPartitionSettings.h
Normal file
36
Source/VerticalPartition/Public/VerticalPartitionSettings.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// Project-level defaults + global debug toggles. Appears under
|
||||||
|
// Project Settings -> Plugins -> Vertical Partition. Per-volume overrides live on
|
||||||
|
// AVerticalPartitionVolume; this provides the defaults a freshly placed volume gets.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DeveloperSettings.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionSettings.generated.h"
|
||||||
|
|
||||||
|
UCLASS(Config=Game, DefaultConfig, meta=(DisplayName="Vertical Partition"))
|
||||||
|
class VERTICALPARTITION_API UVerticalPartitionDeveloperSettings : public UDeveloperSettings
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UVerticalPartitionDeveloperSettings();
|
||||||
|
|
||||||
|
virtual FName GetCategoryName() const override { return FName(TEXT("Plugins")); }
|
||||||
|
|
||||||
|
/** Defaults applied to a newly placed AVerticalPartitionVolume. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category="Defaults")
|
||||||
|
FVerticalPartitionSettings DefaultSettings;
|
||||||
|
|
||||||
|
/** Master debug switch mirrored by the vp.Debug console variable. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category="Debug")
|
||||||
|
bool bDebugEnabledByDefault = false;
|
||||||
|
|
||||||
|
/** Frame time (ms) above which a streaming update is logged as a hitch. */
|
||||||
|
UPROPERTY(Config, EditAnywhere, Category="Debug", meta=(ClampMin="1.0", Units="ms"))
|
||||||
|
float HitchThresholdMS = 8.0f;
|
||||||
|
|
||||||
|
static const UVerticalPartitionDeveloperSettings* Get();
|
||||||
|
};
|
||||||
71
Source/VerticalPartition/Public/VerticalPartitionStatics.h
Normal file
71
Source/VerticalPartition/Public/VerticalPartitionStatics.h
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// Pure cell math shared by the editor builder and the runtime streamer. No state.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionStatics.generated.h"
|
||||||
|
|
||||||
|
/** Context the desired-state policy needs beyond the raw cell index. */
|
||||||
|
struct FVerticalStreamingContext
|
||||||
|
{
|
||||||
|
int32 PlayerCellZ = 0;
|
||||||
|
int32 PlayerCellX = 0;
|
||||||
|
int32 PlayerCellY = 0;
|
||||||
|
float PlayerVelocityZ = 0.f; // cm/s, +up
|
||||||
|
float PlayerWorldZ = 0.f; // cm, exact (for hysteresis at boundaries)
|
||||||
|
float CameraPitchDeg = 0.f; // +looking up
|
||||||
|
bool bUseXY = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class VERTICALPARTITION_API UVerticalPartitionStatics : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Z index of a world Z (cm) relative to the volume floor. */
|
||||||
|
UFUNCTION(BlueprintPure, Category="VerticalPartition")
|
||||||
|
static int32 GetCellZFromWorldZ(float WorldZ, float VolumeMinZ, float CellHeight);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category="VerticalPartition")
|
||||||
|
static int32 GetCellXFromWorldX(float WorldX, float VolumeMinX, float XYCellSize);
|
||||||
|
UFUNCTION(BlueprintPure, Category="VerticalPartition")
|
||||||
|
static int32 GetCellYFromWorldY(float WorldY, float VolumeMinY, float XYCellSize);
|
||||||
|
|
||||||
|
/** Cell id for a single point (actor origin). */
|
||||||
|
static FVerticalCellId GetCellIdFromPoint(const FVector& Point, const FBox& VolumeBounds, const FVerticalPartitionSettings& S);
|
||||||
|
|
||||||
|
/** Cell id for a bounds box. Uses the dominant-overlap cell (center of the
|
||||||
|
* clamped intersection) so a box straddling a boundary lands deterministically.
|
||||||
|
* bOutCrossesMultiple reports whether the box spans more than one cell. */
|
||||||
|
static FVerticalCellId GetCellIdFromBounds(const FBox& ActorBounds, const FBox& VolumeBounds, const FVerticalPartitionSettings& S, bool& bOutCrossesMultiple);
|
||||||
|
|
||||||
|
/** World-space bounds of a cell, clamped to the volume when requested. */
|
||||||
|
static FBox GetCellBounds(const FVerticalCellId& Cell, const FBox& VolumeBounds, const FVerticalPartitionSettings& S);
|
||||||
|
|
||||||
|
/** Number of Z cells the volume spans (>=1). */
|
||||||
|
static int32 GetNumZCells(const FBox& VolumeBounds, const FVerticalPartitionSettings& S);
|
||||||
|
|
||||||
|
/** THE policy. Maps a cell relative to the player to its desired representation,
|
||||||
|
* honouring full/preload/HLOD/unload bands, hysteresis, velocity prediction,
|
||||||
|
* fall-risk-below, camera direction and the above-player priority bias.
|
||||||
|
* CurrentState is the cell's live state and is what makes hysteresis sticky. */
|
||||||
|
static EVerticalCellDesiredState GetDesiredStateForCell(
|
||||||
|
const FVerticalCellId& Cell,
|
||||||
|
const FVerticalStreamingContext& Ctx,
|
||||||
|
const FVerticalPartitionSettings& S,
|
||||||
|
EVerticalCellState CurrentState = EVerticalCellState::Unloaded);
|
||||||
|
|
||||||
|
/** True when a cell sits in the preload band (load full data, keep it hidden). */
|
||||||
|
static bool ShouldPreloadFull(const FVerticalCellId& Cell, const FVerticalStreamingContext& Ctx, const FVerticalPartitionSettings& S);
|
||||||
|
|
||||||
|
/** Human-readable colour for a runtime state (debug draw / overlay). */
|
||||||
|
UFUNCTION(BlueprintPure, Category="VerticalPartition")
|
||||||
|
static FLinearColor GetStateColor(EVerticalCellState State);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category="VerticalPartition")
|
||||||
|
static FString WarningTypeToString(EVerticalWarningType Type);
|
||||||
|
};
|
||||||
486
Source/VerticalPartition/Public/VerticalPartitionTypes.h
Normal file
486
Source/VerticalPartition/Public/VerticalPartitionTypes.h
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// Shared data contract for the whole plugin: cell identity, states, the tunable
|
||||||
|
// settings block, descriptors, and build/runtime report structs.
|
||||||
|
//
|
||||||
|
// This is a BOUNDED VERTICAL ROUTE partition system for a hand-crafted spiral
|
||||||
|
// climbing map. It is NOT an infinite open-world (World Partition) system and it
|
||||||
|
// does NOT use Level Instances as its core architecture.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "VerticalPartitionTypes.generated.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cell identity
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Integer grid coordinate of a cell. Z is the primary (vertical) axis; X/Y are
|
||||||
|
* only used when bUseXYSubCells is enabled. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalCellId
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
int32 X = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
int32 Y = 0;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
int32 Z = 0;
|
||||||
|
|
||||||
|
FVerticalCellId() = default;
|
||||||
|
FVerticalCellId(int32 InX, int32 InY, int32 InZ) : X(InX), Y(InY), Z(InZ) {}
|
||||||
|
|
||||||
|
bool operator==(const FVerticalCellId& Other) const
|
||||||
|
{
|
||||||
|
return X == Other.X && Y == Other.Y && Z == Other.Z;
|
||||||
|
}
|
||||||
|
bool operator!=(const FVerticalCellId& Other) const { return !(*this == Other); }
|
||||||
|
|
||||||
|
FString ToString() const { return FString::Printf(TEXT("X%d_Y%d_Z%d"), X, Y, Z); }
|
||||||
|
|
||||||
|
/** Stable, sortable label used for generated package names ("Cell_Z0010" / "Cell_X0_Y0_Z0010"). */
|
||||||
|
FString ToLabel(bool bWithXY) const
|
||||||
|
{
|
||||||
|
return bWithXY
|
||||||
|
? FString::Printf(TEXT("Cell_X%d_Y%d_Z%04d"), X, Y, Z)
|
||||||
|
: FString::Printf(TEXT("Cell_Z%04d"), Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
friend uint32 GetTypeHash(const FVerticalCellId& C)
|
||||||
|
{
|
||||||
|
return HashCombine(HashCombine(::GetTypeHash(C.X), ::GetTypeHash(C.Y)), ::GetTypeHash(C.Z));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// State machine enums
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Live runtime state of a cell inside the streaming state machine. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVerticalCellState : uint8
|
||||||
|
{
|
||||||
|
Unloaded,
|
||||||
|
LoadingHLOD,
|
||||||
|
HLOD,
|
||||||
|
PreloadingFull,
|
||||||
|
LoadingFull,
|
||||||
|
Full,
|
||||||
|
Unloading,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The representation the streamer *wants* a cell to be in this update. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVerticalCellDesiredState : uint8
|
||||||
|
{
|
||||||
|
Unloaded,
|
||||||
|
HLOD,
|
||||||
|
Full
|
||||||
|
};
|
||||||
|
|
||||||
|
/** How Build/Convert mutates the source level. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVerticalConversionMode : uint8
|
||||||
|
{
|
||||||
|
/** Source level untouched. Cells stream by toggling the existing in-level actors
|
||||||
|
* (visibility/collision/tick) + showing proxies. Safe, no memory savings. */
|
||||||
|
NonDestructive,
|
||||||
|
/** Actors are baked out into per-cell streaming sub-level packages and removed
|
||||||
|
* from the persistent level. True memory streaming. A backup is taken first. */
|
||||||
|
DestructiveCommit
|
||||||
|
};
|
||||||
|
|
||||||
|
/** What to do with an actor that does not fit cleanly into a single cell. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class ELargeActorPolicy : uint8
|
||||||
|
{
|
||||||
|
/** Leave it in the persistent (always-loaded) level. */
|
||||||
|
KeepInPersistentLevel,
|
||||||
|
/** Assign it to the cell that owns the largest fraction of its bounds. */
|
||||||
|
AssignToDominantCell,
|
||||||
|
/** Keep it only as part of HLOD proxies, never as a full streamed actor. */
|
||||||
|
DuplicateAsHLODOnly,
|
||||||
|
/** Emit a warning — splitting a single actor across cells is not supported. */
|
||||||
|
SplitNotSupportedWarning,
|
||||||
|
/** Drop it from partitioning entirely. */
|
||||||
|
ExcludeFromPartition
|
||||||
|
};
|
||||||
|
|
||||||
|
/** How a cell's HLOD proxy is baked from its static geometry. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVerticalHLODMethod : uint8
|
||||||
|
{
|
||||||
|
/** One merged static mesh, original materials preserved (exact silhouette, heavier).
|
||||||
|
* HISM/ISM instances are flattened into the single mesh. */
|
||||||
|
MergedMesh,
|
||||||
|
/** Proxy LOD: simplified geometry + a single baked atlas material (cheapest, true HLOD).
|
||||||
|
* Requires the ProxyLODPlugin to be enabled; falls back to MergedMesh otherwise. */
|
||||||
|
SimplifiedProxy
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVerticalIssueSeverity : uint8
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Classifies an analysis/validation finding so UI can group and filter them. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EVerticalWarningType : uint8
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
ActorCrossesMultipleCells,
|
||||||
|
ActorTooLarge,
|
||||||
|
ActorSimulatesPhysics,
|
||||||
|
ActorHasBlueprintTick,
|
||||||
|
ActorDynamicMobility,
|
||||||
|
ActorHasExternalReferences,
|
||||||
|
ActorAttachedAcrossCells,
|
||||||
|
ActorDependsOnOtherCell,
|
||||||
|
ActorNeedsManualReview,
|
||||||
|
ActorHasAudioLightNav,
|
||||||
|
ActorNotStreamSafe,
|
||||||
|
ActorHasUnsavedChanges,
|
||||||
|
CellTooManyActors,
|
||||||
|
CellTooManyDrawCalls,
|
||||||
|
HLODGenerationFailed,
|
||||||
|
PackageSaveFailed,
|
||||||
|
NoVolume,
|
||||||
|
DescriptorOutOfDate
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Editor action dispatched from the volume's detail-panel buttons / console. */
|
||||||
|
UENUM()
|
||||||
|
enum class EVerticalPartitionAction : uint8
|
||||||
|
{
|
||||||
|
Analyze,
|
||||||
|
Build,
|
||||||
|
RebuildChangedCells,
|
||||||
|
RebuildHLOD,
|
||||||
|
Validate,
|
||||||
|
PreviewChunks,
|
||||||
|
ClearGenerated,
|
||||||
|
Commit,
|
||||||
|
RestoreBackup,
|
||||||
|
DumpReport,
|
||||||
|
SpawnHLODPreview,
|
||||||
|
ClearPreview
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tunable settings (one flat, category-grouped block reused everywhere)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Everything tunable about segmentation, runtime streaming ranges, conversion
|
||||||
|
* and debug. Embedded in AVerticalPartitionVolume (per-volume) and snapshotted
|
||||||
|
* into UVerticalPartitionDescriptor at build time. Defaults are provided by
|
||||||
|
* UVerticalPartitionDeveloperSettings (project settings). */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalPartitionSettings
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
// ===== Segmentation =====
|
||||||
|
/** Height (cm) of one Z cell. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation", meta=(ClampMin="100.0", Units="cm"))
|
||||||
|
float CellHeight = 5000.f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
bool bUseXYSubCells = false;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation", meta=(EditCondition="bUseXYSubCells", ClampMin="100.0", Units="cm"))
|
||||||
|
float XYCellSize = 10000.f;
|
||||||
|
|
||||||
|
/** Snap the generated grid to the volume bounds (no cells outside the volume). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
bool bClampToVolumeBounds = true;
|
||||||
|
|
||||||
|
/** Only actors whose origin/bounds fall inside the volume are partitioned. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
bool bIncludeOnlyActorsInsideVolume = true;
|
||||||
|
|
||||||
|
/** Assign by full actor bounds (true) or by actor origin point (false). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
bool bUseActorBoundsInsteadOfOrigin = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
ELargeActorPolicy LargeActorPolicy = ELargeActorPolicy::AssignToDominantCell;
|
||||||
|
|
||||||
|
/** An actor is "large" when its Z extent exceeds this many cell heights. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation", meta=(ClampMin="1.0"))
|
||||||
|
float LargeActorCellSpan = 1.5f;
|
||||||
|
|
||||||
|
/** SAFETY (recommended ON): only stream pure STATIC DECOR. Actors with lights,
|
||||||
|
* sky atmosphere / Ultra Dynamic Sky, sky-light, fog, post-process, audio,
|
||||||
|
* particles, reflection captures, decals, physics, replication, movable mobility,
|
||||||
|
* and Level Instances are kept in the persistent level and are NEVER hidden by
|
||||||
|
* streaming. Turn OFF only if every actor inside the volume is safe to hide. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
bool bStreamOnlyStaticDecor = true;
|
||||||
|
|
||||||
|
/** Treat Level Instances / Packed Level Actors as streamable units: their static
|
||||||
|
* geometry (incl. HISM/ISM, flattened recursively) is baked into per-cell HLOD and
|
||||||
|
* their static child actors are hidden/shown by streaming. Lights / gameplay inside
|
||||||
|
* an LI stay persistent. Off => LIs are left fully persistent. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Segmentation")
|
||||||
|
bool bStreamLevelInstances = true;
|
||||||
|
|
||||||
|
// ===== Runtime streaming ranges (in cells, measured from the player cell) =====
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Full", meta=(ClampMin="0"))
|
||||||
|
int32 FullLoadCellsBelow = 1;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Full", meta=(ClampMin="0"))
|
||||||
|
int32 FullLoadCellsAbove = 1;
|
||||||
|
|
||||||
|
/** Begin async-loading full data (kept resident, not yet shown) this many cells out. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Preload", meta=(ClampMin="0"))
|
||||||
|
int32 PreloadCellsBelow = 2;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Preload", meta=(ClampMin="0"))
|
||||||
|
int32 PreloadCellsAbove = 3;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|HLOD", meta=(ClampMin="0"))
|
||||||
|
int32 HLODLoadCellsBelow = 4;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|HLOD", meta=(ClampMin="0"))
|
||||||
|
int32 HLODLoadCellsAbove = 5;
|
||||||
|
|
||||||
|
/** Beyond this many cells the cell is fully unloaded. Should be >= HLOD range. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Unload", meta=(ClampMin="0"))
|
||||||
|
int32 UnloadCellsBelow = 5;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Unload", meta=(ClampMin="0"))
|
||||||
|
int32 UnloadCellsAbove = 6;
|
||||||
|
|
||||||
|
/** Z distance (cm) the player must move past a boundary before a downgrade fires. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Hysteresis", meta=(ClampMin="0.0", Units="cm"))
|
||||||
|
float HysteresisDistanceZ = 500.f;
|
||||||
|
|
||||||
|
/** Extra cells kept at their current (higher) state to avoid thrash near edges. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Hysteresis", meta=(ClampMin="0"))
|
||||||
|
int32 HysteresisCells = 1;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="0.0", Units="s"))
|
||||||
|
float UpdateInterval = 0.2f;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="1"))
|
||||||
|
int32 MaxAsyncLoadsPerFrame = 1;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="1"))
|
||||||
|
int32 MaxAsyncUnloadsPerFrame = 1;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="0"))
|
||||||
|
int32 MaxVisibleHLODCells = 24;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Budget", meta=(ClampMin="0"))
|
||||||
|
int32 MaxFullCells = 8;
|
||||||
|
|
||||||
|
// ===== Priority / prediction =====
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
|
||||||
|
bool bPrioritizeAbovePlayer = true;
|
||||||
|
/** Keep cells below the player loaded longer (the player can always fall back down). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
|
||||||
|
bool bKeepBelowPlayerLoadedOnFallRisk = true;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
|
||||||
|
bool bUseCameraDirectionPriority = false;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority")
|
||||||
|
bool bUsePlayerVelocityPrediction = true;
|
||||||
|
/** How many seconds of velocity to extrapolate the player position for prediction. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Streaming|Priority", meta=(ClampMin="0.0", Units="s"))
|
||||||
|
float VelocityPredictionSeconds = 0.75f;
|
||||||
|
|
||||||
|
// ===== Conversion =====
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
|
||||||
|
EVerticalConversionMode ConversionMode = EVerticalConversionMode::NonDestructive;
|
||||||
|
|
||||||
|
/** Content root for generated cell maps / proxy meshes / descriptor. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
|
||||||
|
FString GeneratedPackageRoot = TEXT("/Game/VPGenerated");
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
|
||||||
|
bool bCreateBackup = true;
|
||||||
|
|
||||||
|
/** When set, only actors of these classes are considered for partitioning. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
|
||||||
|
TArray<TSoftClassPtr<AActor>> IncludeOnlyClasses;
|
||||||
|
|
||||||
|
/** Actors of these classes are always excluded from partitioning. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Conversion")
|
||||||
|
TArray<TSoftClassPtr<AActor>> ExcludeClasses;
|
||||||
|
|
||||||
|
// ===== HLOD =====
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD")
|
||||||
|
bool bGenerateHLODProxies = true;
|
||||||
|
/** Only static-mesh actors below this draw-call-ish triangle budget are merged. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD", meta=(ClampMin="0"))
|
||||||
|
int32 HLODMaxSourceActorsPerCell = 4096;
|
||||||
|
|
||||||
|
/** MergedMesh (default, exact) or SimplifiedProxy (cheaper, needs ProxyLODPlugin). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD")
|
||||||
|
EVerticalHLODMethod HLODMethod = EVerticalHLODMethod::MergedMesh;
|
||||||
|
|
||||||
|
/** Target screen size (px) for SimplifiedProxy generation. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HLOD", meta=(ClampMin="1", ClampMax="1200", EditCondition="HLODMethod==EVerticalHLODMethod::SimplifiedProxy"))
|
||||||
|
int32 ProxyScreenSize = 300;
|
||||||
|
|
||||||
|
// ===== Debug =====
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bDrawVolumeBounds = true;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bDrawZSlices = true;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bDrawXYCells = false;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bDrawCellIds = true;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bDrawActorCounts = true;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bDrawProblemActors = true;
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
|
||||||
|
bool bShowRuntimeOverlay = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Descriptor data (serialized in UVerticalPartitionDescriptor)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** One generated cell: bounds, the package that holds its full actors, its proxy
|
||||||
|
* asset, and the source actors that fed it. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalCellDescriptor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FVerticalCellId CellId;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FBox Bounds = FBox(ForceInit);
|
||||||
|
|
||||||
|
/** Streaming sub-level package for the full representation (DestructiveCommit only). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FSoftObjectPath FullCellPackage;
|
||||||
|
|
||||||
|
/** Offline-generated proxy static mesh shown while the cell is in HLOD range. */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FSoftObjectPath HLODProxyAsset;
|
||||||
|
|
||||||
|
/** Soft paths to the original actors assigned to this cell (NonDestructive backend). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
TArray<FSoftObjectPath> SourceActors;
|
||||||
|
|
||||||
|
/** Actors in *other* cells that this cell hard-references (cross-cell dependencies). */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
TArray<FSoftObjectPath> Dependencies;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 ActorCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 StaticMeshCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 BlueprintActorCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 LightCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 DynamicActorCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
float EstimatedMemoryMB = 0.f;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
bool bHasWarnings = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A single analysis / validation finding. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalPartitionWarning
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
EVerticalWarningType Type = EVerticalWarningType::None;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
EVerticalIssueSeverity Severity = EVerticalIssueSeverity::Warning;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FString Message;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FSoftObjectPath Actor;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FVerticalCellId Cell;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** An error is a finding that blocks (or partially blocks) the build. Same shape. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalPartitionError
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
EVerticalWarningType Type = EVerticalWarningType::None;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FString Message;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FSoftObjectPath Actor;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FVerticalCellId Cell;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Result of Analyze / Build, surfaced in the editor UI and dumpable to a report. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalPartitionBuildReport
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 TotalActorsScanned = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 ActorsInsideVolume = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 ActorsExcluded = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 GeneratedCells = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 EmptyCells = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 WarningCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 ErrorCount = 0;
|
||||||
|
|
||||||
|
/** The Z cell (and XY sub-cell) with the most actors, for the summary panel. */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 LargestCellActorCount = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
FVerticalCellId LargestCell;
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
TArray<FVerticalPartitionWarning> Warnings;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
TArray<FVerticalPartitionError> Errors;
|
||||||
|
|
||||||
|
void Reset() { *this = FVerticalPartitionBuildReport(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Live runtime counters surfaced by the debug overlay / vp.DumpState. */
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FVerticalRuntimeStats
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 FullCellsLoaded = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 HLODCellsVisible = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 CellsUnloaded = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 CellsLoading = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 ActiveAsyncLoads = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
float EstimatedMemoryMB = 0.f;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
float LastUpdateTimeMS = 0.f;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
float LastStreamingTimeMS = 0.f;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 HitchesOverThreshold = 0;
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
int32 PlayerCellZ = 0;
|
||||||
|
};
|
||||||
89
Source/VerticalPartition/Public/VerticalPartitionVolume.h
Normal file
89
Source/VerticalPartition/Public/VerticalPartitionVolume.h
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
//
|
||||||
|
// The authoring volume. Drop one around the whole playable spiral, tune the
|
||||||
|
// settings, and press Build. Bounds come from a UBoxComponent so they are trivial
|
||||||
|
// to read at both editor and runtime (no BSP brush).
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "VerticalPartitionTypes.h"
|
||||||
|
#include "VerticalPartitionVolume.generated.h"
|
||||||
|
|
||||||
|
class UBoxComponent;
|
||||||
|
class UVerticalPartitionDescriptor;
|
||||||
|
class AVerticalPartitionVolume;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decouples the runtime volume from the editor-only builder. The editor module
|
||||||
|
* installs Handler in StartupModule; the volume's CallInEditor buttons and the
|
||||||
|
* vp.* build console commands route through it. Lives in the runtime module so the
|
||||||
|
* volume can reference it, but is only ever populated in editor builds.
|
||||||
|
*/
|
||||||
|
struct VERTICALPARTITION_API FVerticalPartitionEditorBridge
|
||||||
|
{
|
||||||
|
#if WITH_EDITOR
|
||||||
|
static TFunction<void(AVerticalPartitionVolume*, EVerticalPartitionAction)> Handler;
|
||||||
|
static void Execute(AVerticalPartitionVolume* Volume, EVerticalPartitionAction Action)
|
||||||
|
{
|
||||||
|
if (Handler) { Handler(Volume, Action); }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(HideCategories=(Collision, Physics, Navigation, HLOD, Input, Replication))
|
||||||
|
class VERTICALPARTITION_API AVerticalPartitionVolume : public AActor
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
AVerticalPartitionVolume();
|
||||||
|
|
||||||
|
/** Box that defines the bounded playable region to partition. */
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="VerticalPartition")
|
||||||
|
TObjectPtr<UBoxComponent> Box;
|
||||||
|
|
||||||
|
/** Per-volume settings (seeded from project defaults on placement). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition", meta=(ShowOnlyInnerProperties))
|
||||||
|
FVerticalPartitionSettings Settings;
|
||||||
|
|
||||||
|
/** Descriptor produced by Build; consumed by the runtime manager. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="VerticalPartition")
|
||||||
|
TSoftObjectPtr<UVerticalPartitionDescriptor> Descriptor;
|
||||||
|
|
||||||
|
/** World-space bounds of the volume (box transform * extent). */
|
||||||
|
UFUNCTION(BlueprintPure, Category="VerticalPartition")
|
||||||
|
FBox GetVolumeBounds() const;
|
||||||
|
|
||||||
|
virtual void OnConstruction(const FTransform& Transform) override;
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
// ---- Detail-panel buttons (mirror the editor tool window) ----
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|1 - Analyze")
|
||||||
|
void AnalyzeCurrentLevel();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|2 - Build")
|
||||||
|
void BuildVerticalPartition();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|2 - Build")
|
||||||
|
void RebuildChangedCells();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|2 - Build")
|
||||||
|
void RebuildHLOD();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
|
||||||
|
void ValidateCells();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
|
||||||
|
void PreviewChunks();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
|
||||||
|
void SpawnHLODPreview();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|3 - Validate")
|
||||||
|
void ClearPreview();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|4 - Commit")
|
||||||
|
void CommitConversion();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|4 - Commit")
|
||||||
|
void RestoreFromBackup();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|5 - Maintenance")
|
||||||
|
void ClearGeneratedData();
|
||||||
|
UFUNCTION(CallInEditor, Category="Vertical Partition|5 - Maintenance")
|
||||||
|
void DumpReport();
|
||||||
|
|
||||||
|
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
30
Source/VerticalPartition/VerticalPartition.Build.cs
Normal file
30
Source/VerticalPartition/VerticalPartition.Build.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright IHY. Vertical Partition Streaming plugin.
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class VerticalPartition : ModuleRules
|
||||||
|
{
|
||||||
|
public VerticalPartition(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"DeveloperSettings",
|
||||||
|
});
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.AddRange(new string[]
|
||||||
|
{
|
||||||
|
"RenderCore",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Editor-only helpers compiled into the runtime module are guarded by WITH_EDITOR;
|
||||||
|
// the heavy editor build logic lives in the VerticalPartitionEditor module.
|
||||||
|
if (Target.bBuildEditor)
|
||||||
|
{
|
||||||
|
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1005
Source/VerticalPartitionEditor/Private/VerticalPartitionBuilder.cpp
Normal file
1005
Source/VerticalPartitionEditor/Private/VerticalPartitionBuilder.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
109
USAGE_RU.md
Normal file
109
USAGE_RU.md
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Vertical Partition — практическая инструкция
|
||||||
|
|
||||||
|
> Пошаговое «что нажимать». Архитектура и детали — в [README.md](README.md).
|
||||||
|
|
||||||
|
Плагин берёт **готовую** вертикальную карту (Only Up / Only Climb), режет её на
|
||||||
|
**Z-ячейки**, печёт офлайн **HLOD-прокси** и в рантайме грузит ближние ячейки целиком,
|
||||||
|
дальние показывает как прокси, совсем дальние выгружает.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Один раз: включить плагин
|
||||||
|
Плагин `EnabledByDefault` — подхватывается сам. Если нет: `Edit → Plugins → Vertical
|
||||||
|
Partition → Enabled`, перезапустить редактор. После C++-правок нужна полная пересборка
|
||||||
|
(новые модули в редактор «на лету» не подгружаются).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Поставить том вокруг трассы
|
||||||
|
1. В уровне: **Place Actors → ищи `Vertical Partition Volume`** → перетащи на сцену.
|
||||||
|
2. Масштабируй **Box** так, чтобы он охватил **всю** играбельную спираль (низ → верх).
|
||||||
|
Бокс рисуется голубым; коллизии у него нет.
|
||||||
|
|
||||||
|
## 2. Настроить (в деталях тома, секция `Vertical Partition → Settings`)
|
||||||
|
Минимум, что нужно тронуть:
|
||||||
|
- **Cell Height** — высота одной ячейки (см). Ориентир: 1–2 «этажа» трассы. Например `5000`.
|
||||||
|
- **Full/HLOD/Unload ranges** (категория `Streaming`) — сколько ячеек вокруг игрока держать:
|
||||||
|
- `Full Load Cells Below/Above` — целиком (обычно 1/1).
|
||||||
|
- `HLOD Load Cells Below/Above` — прокси (например 4/5).
|
||||||
|
- `Unload Cells Below/Above` — дальше выгрузка.
|
||||||
|
- Остальное можно оставить по умолчанию.
|
||||||
|
|
||||||
|
> **Важно про LI:** если трасса собрана из **Level Instance** — они должны быть
|
||||||
|
> **ЗАГРУЖЕНЫ** в редакторе (не свёрнуты) перед сборкой. Незагруженный LI пропустится
|
||||||
|
> с варнингом «load it and rebuild».
|
||||||
|
|
||||||
|
## 3. Анализ (без записи)
|
||||||
|
Кнопка **`1 - Analyze → Analyze Current Level`**.
|
||||||
|
Смотри лог (Output Log, фильтр `LogVerticalPartition`): сколько акторов внутри тома,
|
||||||
|
сколько ячеек, сколько ушло в persistent, варнинги. Ничего не меняется на диске.
|
||||||
|
|
||||||
|
## 4. Сборка
|
||||||
|
Кнопка **`2 - Build → Build Vertical Partition`**.
|
||||||
|
Что произойдёт:
|
||||||
|
- бэкап карты в `Saved/VerticalPartition/Backups/`;
|
||||||
|
- генерится дескриптор `/Game/VPGenerated/<Карта>/VPD_<Карта>`;
|
||||||
|
- пекутся HLOD-прокси в `…/Proxies/`;
|
||||||
|
- в уровень добавляется актор-менеджер (`Vertical Partition Manager`);
|
||||||
|
- том получает ссылку на дескриптор.
|
||||||
|
|
||||||
|
## 5. Проверить HLOD во вьюпорте (без запуска игры)
|
||||||
|
- **`3 - Validate → Preview Chunks`** — цветные боксы ячеек:
|
||||||
|
- 🟢 **зелёный** = у ячейки есть HLOD-прокси;
|
||||||
|
- 🟠 **оранжевый** = стримится, но прокси нет (просто прячется вдали);
|
||||||
|
- ⚪ **серый** = пусто / persistent.
|
||||||
|
- **`3 - Validate → Spawn HLOD Preview`** — спавнит сами прокси-меши поверх оригинала.
|
||||||
|
Спрячь реальный декор (или посмотри издалека), чтобы оценить силуэт.
|
||||||
|
- **`3 - Validate → Clear Preview`** — убрать боксы и превью-акторы.
|
||||||
|
|
||||||
|
## 6. Запустить и проверить стриминг
|
||||||
|
**Play**. Включи отладку в консоли (`~`):
|
||||||
|
```
|
||||||
|
vp.Debug 1
|
||||||
|
vp.DebugDraw 1 ; цветные боксы ячеек в 3D
|
||||||
|
vp.DebugOverlay 1 ; статистика на экране
|
||||||
|
```
|
||||||
|
Цвета: 🟢 Full · 🔵 HLOD · 🟡 грузится · 🔴 ошибка · ⚪ выгружено · 🟣 forced.
|
||||||
|
Полезные команды: `vp.DumpCells`, `vp.TeleportToCell 12`, `vp.ForceCellHLOD 8`,
|
||||||
|
`vp.SetFullRange 2 2`, `vp.ProfileStreaming`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что стримится, а что НЕТ (запомни)
|
||||||
|
Стримится (прячется/заменяется прокси) **только чистый статик-декор**.
|
||||||
|
**Никогда не трогаются** (всегда persistent, всегда видны):
|
||||||
|
- свет, **Ultra Dynamic Sky**, Sky Atmosphere, SkyLight, туман, post-process;
|
||||||
|
- аудио, частицы/Niagara, reflection capture, декали;
|
||||||
|
- физика, реплицируемые, Movable-акторы;
|
||||||
|
- свет/геймплей **внутри** Level Instance.
|
||||||
|
|
||||||
|
→ Поэтому небо и рабочие акторы **не пропадают**. Управляется флагом
|
||||||
|
`Settings → bStreamOnlyStaticDecor` (вкл по умолчанию — не выключай без причины).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Если что-то «пропало» / нужно откатить
|
||||||
|
NonDestructive-режим **ничего не удаляет с диска** — прячет только в игре (PIE).
|
||||||
|
- Полный сброс: удали актор `Vertical Partition Manager` из уровня + папку
|
||||||
|
`/Game/VPGenerated/`, затем `Build` заново. Или кнопка **`5 - Maintenance → Clear Generated Data`**.
|
||||||
|
- Восстановить карту из бэкапа: **`4 - Commit → Restore From Backup`**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Частые проблемы
|
||||||
|
| Симптом | Причина / решение |
|
||||||
|
|---|---|
|
||||||
|
| Небо погасло / акторы исчезли в Play | Старый дескриптор до фикса. **Build заново** (теперь свет/небо/LI остаются persistent). |
|
||||||
|
| Level Instance не попал в чанки | LI был свёрнут. Загрузи его в редакторе → `Build`. |
|
||||||
|
| Ячейки все оранжевые (нет HLOD) | В ячейке нет статик-геометрии, либо `bGenerateHLODProxies` выключен. |
|
||||||
|
| `SimplifiedProxy` ничего не печёт | Нужен включённый **ProxyLODPlugin**. Без него авто-откат на `MergedMesh` (это ок). |
|
||||||
|
| Прокси смещён | Не должно — пекутся с пивотом в (0,0,0). Если видишь смещение — пересобери HLOD. |
|
||||||
|
| Хочу перепечь только прокси | **`2 - Build → Rebuild HLOD`**. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Офлайн-сборка (CI / без открытия редактора)
|
||||||
|
```
|
||||||
|
UnrealEditor-Cmd.exe IHY.uproject -run=VerticalPartition -Map=/Game/Maps/MyClimbMap
|
||||||
|
```
|
||||||
|
Отчёт пишется в `Saved/VerticalPartition/<Карта>_Report.txt`.
|
||||||
24
VerticalPartition.uplugin
Normal file
24
VerticalPartition.uplugin
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"FileVersion": 3,
|
||||||
|
"Version": 1,
|
||||||
|
"VersionName": "0.1.0",
|
||||||
|
"FriendlyName": "Vertical Partition Streaming",
|
||||||
|
"Description": "Bounded vertical route partition / streaming system for hand-crafted spiral climbing maps (Only Up / Only Climb style). NOT an infinite open-world partition. Converts an existing level into Z-segmented streaming cells with offline HLOD proxies, a vertical-aware runtime streamer, and World-Partition-style debug visualization.",
|
||||||
|
"Category": "World",
|
||||||
|
"CreatedBy": "IHY",
|
||||||
|
"CanContainContent": false,
|
||||||
|
"IsBetaVersion": true,
|
||||||
|
"Installed": false,
|
||||||
|
"Modules": [
|
||||||
|
{
|
||||||
|
"Name": "VerticalPartition",
|
||||||
|
"Type": "Runtime",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "VerticalPartitionEditor",
|
||||||
|
"Type": "Editor",
|
||||||
|
"LoadingPhase": "Default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user