This commit is contained in:
Fr0zka committed 2026-06-16 03:39:22 +02:00
1 parent f030eec08a
commit db558d9e14
18 files changed
+1570 -264

No files matched your search

+158 -28
View File
@@ -4,20 +4,27 @@
#include "VoxelContentManager.h"
#include "VoxelStrateManager.h"
#include "VoxelStrateDefinition.h"
#include "VoxelBiomeDefinition.h"
#include "VoxelGenerator.h"
#include "VoxelCaveMorphology.h" // VoxelHash
#include "Components/StaticMeshComponent.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
#include "Components/LightComponent.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "Materials/MaterialInterface.h"
// Global safety cap on spawned decorations per chunk (across all entries).
// Global safety cap on spawned decoration ACTORS per chunk (across all entries).
// HISM instances are exempt — they're batched render data, capped per entry by MaxPerChunk.
static constexpr int32 GMaxDecorationsPerChunk = 400;
void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager, int32 InSeed)
void UVoxelContentManager::Initialize(AActor* InOwner, UVoxelStrateManager* InStrateManager,
UVoxelGenerator* InGenerator, int32 InSeed)
{
Owner = InOwner;
StrateManager = InStrateManager;
Generator = InGenerator;
Seed = InSeed;
// Engine unit plane — reused (scaled) for every water surface.
@@ -46,25 +53,43 @@ void UVoxelContentManager::PopulateChunk(const FIntVector& ChunkCoord, const FVo
const UVoxelStrateDefinition* Def = StrateManager->GetStrateForChunk(ChunkCoord);
if (!Def) return;
// Decorations only on near (full-detail) chunks — they're game-thread actor spawns
// and pointless on distant low-poly chunks. Water is one cheap plane, place it at any LOD.
if (LODLevel == 0)
// Dominant biome for this chunk (center XY) selects the content + water overrides.
// Per-chunk granularity: a chunk straddling a biome border takes its centre's biome
// for the whole decoration SET (fine — the set is a choice, not a per-vertex blend).
const UVoxelBiomeDefinition* Biome = nullptr;
if (Generator)
{
const float CenterVoxelX = ((float)ChunkCoord.X + 0.5f) * CHUNK_SIZE;
const float CenterVoxelY = ((float)ChunkCoord.Y + 0.5f) * CHUNK_SIZE;
Biome = Generator->GetDominantBiomeAt(CenterVoxelX, CenterVoxelY, ChunkCoord.Z);
}
// Decorations come from the biome when it provides any, else the strate's list.
const TArray<FStrateDecoration>& Decos =
(Biome && Biome->Decorations.Num() > 0) ? Biome->Decorations : Def->Decorations;
// Per-entry LOD gate inside SpawnDecorations (FStrateDecoration::MaxLODLevel):
// actor entries usually stay LOD0-only; instanced entries may persist to LOD1-2.
// Water is one cheap plane, place it at any LOD.
{
TArray<TWeakObjectPtr<AActor>> Spawned;
SpawnDecorations(ChunkCoord, MeshData, Def, Spawned);
SpawnDecorations(ChunkCoord, MeshData, Decos, LODLevel, Spawned);
if (Spawned.Num() > 0)
{
SpawnedActors.Add(ChunkCoord, MoveTemp(Spawned));
}
}
SpawnWater(ChunkCoord, Def);
// Water plane: biome material override (level stays strate-global, see SpawnWater).
UMaterialInterface* WaterMat = (Biome && Biome->WaterMaterial) ? Biome->WaterMaterial : Def->WaterMaterial;
SpawnWater(ChunkCoord, Def, WaterMat);
}
void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const FVoxelMeshData& MeshData,
const UVoxelStrateDefinition* Def, TArray<TWeakObjectPtr<AActor>>& Out)
const TArray<FStrateDecoration>& Decorations, int32 LODLevel,
TArray<TWeakObjectPtr<AActor>>& Out)
{
if (Def->Decorations.Num() == 0) return;
if (Decorations.Num() == 0) return;
AActor* OwnerActor = Owner.Get();
if (!OwnerActor) return;
@@ -81,19 +106,30 @@ void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const
const bool bHasWater = (WaterVoxelZ != -FLT_MAX);
const float WaterWorldZ = bHasWater ? WaterVoxelZ * VOXEL_SIZE : -FLT_MAX;
// Strate light culling: lights only live in the player's strate. INT32_MIN = player
// strate not known yet (first frames) — leave lights on, the first SetActiveStrate
// sweep corrects them.
const int32 ChunkStrate = GetChunkStrateIndex(ChunkCoord);
const bool bLightsOn = (ActiveStrateIndex == INT32_MIN) || (ChunkStrate == ActiveStrateIndex);
int32 TotalSpawned = 0;
for (int32 DecoIdx = 0; DecoIdx < Def->Decorations.Num(); ++DecoIdx)
for (int32 DecoIdx = 0; DecoIdx < Decorations.Num(); ++DecoIdx)
{
const FStrateDecoration& Deco = Def->Decorations[DecoIdx];
if (!Deco.ActorClass) continue;
const FStrateDecoration& Deco = Decorations[DecoIdx];
const bool bInstanced = (Deco.InstancedMesh != nullptr);
if (!bInstanced && !Deco.ActorClass) continue;
if (Deco.SpawnDensity <= 0.0f) continue;
if (LODLevel > Deco.MaxLODLevel) continue; // entry not wanted at this LOD
// Lazily created on the first placement of this entry (avoids empty components).
UHierarchicalInstancedStaticMeshComponent* HISM = nullptr;
int32 SpawnedThisEntry = 0;
for (int32 i = 0; i < NumVerts; ++i)
{
if (TotalSpawned >= GMaxDecorationsPerChunk) return;
if (!bInstanced && TotalSpawned >= GMaxDecorationsPerChunk) break; // actor cap
if (SpawnedThisEntry >= Deco.MaxPerChunk) break;
const FVector NormalWorld = MeshData.Normals.IsValidIndex(i)
@@ -144,26 +180,101 @@ void UVoxelContentManager::SpawnDecorations(const FIntVector& ChunkCoord, const
const float ScaleT = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x5CA1E000u));
const float Scale = FMath::Lerp(Deco.MinScale, Deco.MaxScale, ScaleT);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = OwnerActor;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
const FTransform SpawnXf(BaseQ, SpawnPos, FVector(Scale));
AActor* NewActor = World->SpawnActor<AActor>(
Deco.ActorClass,
FTransform(BaseQ, SpawnPos, FVector(Scale)),
SpawnParams);
if (NewActor)
if (bInstanced)
{
Out.Add(NewActor);
// HISM path — batched instances, no actor. Pure visual: no collision,
// engine handles frustum/occlusion culling per cluster.
if (!HISM)
{
HISM = NewObject<UHierarchicalInstancedStaticMeshComponent>(OwnerActor);
HISM->SetStaticMesh(Deco.InstancedMesh);
HISM->SetMobility(EComponentMobility::Movable);
HISM->SetCollisionEnabled(ECollisionEnabled::NoCollision);
HISM->RegisterComponent();
HISM->AttachToComponent(OwnerActor->GetRootComponent(),
FAttachmentTransformRules::KeepRelativeTransform);
ChunkInstances.FindOrAdd(ChunkCoord).Add(HISM);
}
HISM->AddInstance(SpawnXf, /*bWorldSpace=*/true);
++SpawnedThisEntry;
++TotalSpawned;
}
else
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = OwnerActor;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* NewActor = World->SpawnActor<AActor>(Deco.ActorClass, SpawnXf, SpawnParams);
if (NewActor)
{
// Strate light culling — wrong strate ⇒ lights start hidden.
if (!bLightsOn)
{
SetActorLightsEnabled(NewActor, false);
}
Out.Add(NewActor);
++SpawnedThisEntry;
++TotalSpawned;
}
}
}
}
}
void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def)
//=============================================================================
// STRATE LIGHT CULLING
//=============================================================================
// Seals + the bedrock gap make strates light-tight by construction, but a shadowless
// point light shines straight through rock (blocking IS shadowing), and a shadowed one
// pays full shadow cost to render black. Lights can therefore only legitimately matter
// in the player's own strate — toggle them analytically on strate change instead of
// asking the GPU to discover it per-pixel.
int32 UVoxelContentManager::GetChunkStrateIndex(const FIntVector& ChunkCoord) const
{
if (!StrateManager) return INT32_MIN;
// GetStrateIndex expects Unreal world units (cm) — it converts cm→chunkZ internally.
// (Previously this passed voxel-Z, so the strate match for light culling was wrong.)
const float CenterWorldZ = ((float)ChunkCoord.Z + 0.5f) * CHUNK_SIZE * VOXEL_SIZE;
return StrateManager->GetStrateIndex(CenterWorldZ);
}
void UVoxelContentManager::SetActorLightsEnabled(AActor* Actor, bool bEnabled)
{
TInlineComponentArray<ULightComponent*> Lights(Actor);
for (ULightComponent* Light : Lights)
{
if (Light)
{
Light->SetVisibility(bEnabled);
}
}
}
void UVoxelContentManager::SetActiveStrate(int32 PlayerStrateIndex)
{
if (PlayerStrateIndex == ActiveStrateIndex) return; // every-Tick fast path
ActiveStrateIndex = PlayerStrateIndex;
// Strate change (rare): sweep all populated chunks and toggle their actors' lights.
for (const auto& Pair : SpawnedActors)
{
const bool bEnable = (GetChunkStrateIndex(Pair.Key) == ActiveStrateIndex);
for (const TWeakObjectPtr<AActor>& A : Pair.Value)
{
if (AActor* Act = A.Get())
{
SetActorLightsEnabled(Act, bEnable);
}
}
}
}
void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxelStrateDefinition* Def,
UMaterialInterface* WaterMaterial)
{
if (!Def->bHasWater || !PlaneMesh) return;
@@ -196,9 +307,9 @@ void UVoxelContentManager::SpawnWater(const FIntVector& ChunkCoord, const UVoxel
const float ChunkWorld = (float)CHUNK_SIZE * VOXEL_SIZE;
Plane->SetWorldScale3D(FVector(ChunkWorld / 100.0f, ChunkWorld / 100.0f, 1.0f));
if (Def->WaterMaterial)
if (WaterMaterial)
{
Plane->SetMaterial(0, Def->WaterMaterial);
Plane->SetMaterial(0, WaterMaterial);
}
WaterPlanes.Add(ChunkCoord, Plane);
@@ -218,6 +329,18 @@ void UVoxelContentManager::ClearChunk(const FIntVector& ChunkCoord)
SpawnedActors.Remove(ChunkCoord);
}
if (TArray<TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>>* Comps = ChunkInstances.Find(ChunkCoord))
{
for (const TWeakObjectPtr<UHierarchicalInstancedStaticMeshComponent>& C : *Comps)
{
if (UHierarchicalInstancedStaticMeshComponent* Comp = C.Get())
{
Comp->DestroyComponent();
}
}
ChunkInstances.Remove(ChunkCoord);
}
if (UStaticMeshComponent** Plane = WaterPlanes.Find(ChunkCoord))
{
if (*Plane)
@@ -236,7 +359,13 @@ void UVoxelContentManager::ClearAll()
{
ClearChunk(C);
}
// Any water planes without decorations.
// Chunks that only have instances or water (no actors).
TArray<FIntVector> InstanceCoords;
ChunkInstances.GetKeys(InstanceCoords);
for (const FIntVector& C : InstanceCoords)
{
ClearChunk(C);
}
TArray<FIntVector> WaterCoords;
WaterPlanes.GetKeys(WaterCoords);
for (const FIntVector& C : WaterCoords)
@@ -244,5 +373,6 @@ void UVoxelContentManager::ClearAll()
ClearChunk(C);
}
SpawnedActors.Empty();
ChunkInstances.Empty();
WaterPlanes.Empty();
}