- Created a comprehensive implementation checklist for the Planar Capture System (Systems 136-147) detailing tasks across multiple phases including C++ core, material foundation, Blueprint actors, data assets, integration, and performance testing. - Added a developer reference document outlining the architecture, data flow, state machine, budget enforcement, render target pooling, horror features, integration points, multiplayer networking, performance characteristics, debugging methods, and build order for the capture systems. - Introduced examples of capture surface usage in the Project Void horror game, including specific implementations for mirrors, monitors, portals, and fake windows, along with a checklist for integration tasks.
435 lines
11 KiB
C++
435 lines
11 KiB
C++
// Copyright Ngonart OU. All Rights Reserved.
|
|
// UE5 Modular Game Framework — SS_PlanarCaptureManager implementation
|
|
|
|
#include "Capture/SS_PlanarCaptureManager.h"
|
|
#include "Capture/BP_PlanarCaptureActor.h"
|
|
#include "Capture/BPC_PlanarCapture.h"
|
|
#include "Engine/TextureRenderTarget2D.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
|
|
ASS_PlanarCaptureManager::ASS_PlanarCaptureManager()
|
|
{
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Initialized for world."));
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::Deinitialize()
|
|
{
|
|
// Release all render targets
|
|
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
|
{
|
|
if (Entry.RenderTarget)
|
|
{
|
|
Entry.RenderTarget->ConditionalBeginDestroy();
|
|
}
|
|
}
|
|
RenderTargetPool.Empty();
|
|
|
|
RegisteredSurfaces.Empty();
|
|
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::Tick(float DeltaTime)
|
|
{
|
|
Super::Tick(DeltaTime);
|
|
|
|
TimeSinceLastEvaluation += DeltaTime;
|
|
|
|
if (TimeSinceLastEvaluation >= FullEvaluationInterval)
|
|
{
|
|
TimeSinceLastEvaluation = 0.0f;
|
|
EvaluateAllSurfaces();
|
|
}
|
|
}
|
|
|
|
TStatId ASS_PlanarCaptureManager::GetStatId() const
|
|
{
|
|
RETURN_QUICK_DECLARE_CYCLE_STAT(ASS_PlanarCaptureManager, STATGROUP_Tickables);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Surface Registry
|
|
// ========================================================================
|
|
|
|
void ASS_PlanarCaptureManager::RegisterSurface(ABP_PlanarCaptureActor* Surface)
|
|
{
|
|
if (!Surface)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check for duplicates
|
|
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Existing : RegisteredSurfaces)
|
|
{
|
|
if (Existing.Get() == Surface)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Surface '%s' already registered."),
|
|
*Surface->SurfaceDisplayName);
|
|
return;
|
|
}
|
|
}
|
|
|
|
RegisteredSurfaces.Add(Surface);
|
|
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Registered surface '%s'. Total: %d"),
|
|
*Surface->SurfaceDisplayName, RegisteredSurfaces.Num());
|
|
|
|
OnSurfaceRegistered.Broadcast(Surface, RegisteredSurfaces.Num());
|
|
|
|
// Evaluate immediately to assign initial tier
|
|
EvaluateAllSurfaces();
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::UnregisterSurface(ABP_PlanarCaptureActor* Surface)
|
|
{
|
|
if (!Surface)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RegisteredSurfaces.RemoveAll([Surface](const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry)
|
|
{
|
|
return Entry.Get() == Surface;
|
|
});
|
|
|
|
UE_LOG(LogTemp, Log, TEXT("SS_PlanarCaptureManager: Unregistered surface '%s'. Total: %d"),
|
|
*Surface->SurfaceDisplayName, RegisteredSurfaces.Num());
|
|
|
|
OnSurfaceUnregistered.Broadcast(Surface, RegisteredSurfaces.Num());
|
|
}
|
|
|
|
TArray<ABP_PlanarCaptureActor*> ASS_PlanarCaptureManager::GetRegisteredSurfaces() const
|
|
{
|
|
TArray<ABP_PlanarCaptureActor*> Result;
|
|
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry : RegisteredSurfaces)
|
|
{
|
|
if (Entry.IsValid())
|
|
{
|
|
Result.Add(Entry.Get());
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Quality Budget Management
|
|
// ========================================================================
|
|
|
|
void ASS_PlanarCaptureManager::ForceAllSurfacesToTier(EPlanarCaptureQualityTier Tier)
|
|
{
|
|
ForceTierOverride = Tier;
|
|
|
|
for (TWeakObjectPtr<ABP_PlanarCaptureActor>& SurfaceWeak : RegisteredSurfaces)
|
|
{
|
|
if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get())
|
|
{
|
|
if (UBPC_PlanarCapture* Capture = Surface->CaptureComponent)
|
|
{
|
|
Capture->ApplyQualityTier(Tier);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::ReleaseForceTier()
|
|
{
|
|
ForceTierOverride.Reset();
|
|
EvaluateAllSurfaces();
|
|
}
|
|
|
|
// ========================================================================
|
|
// Render Target Pool
|
|
// ========================================================================
|
|
|
|
UTextureRenderTarget2D* ASS_PlanarCaptureManager::RequestRenderTarget(int32 Size)
|
|
{
|
|
// Check pool for an available RT of the right size
|
|
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
|
{
|
|
if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget)
|
|
{
|
|
Entry.bInUse = true;
|
|
return Entry.RenderTarget;
|
|
}
|
|
}
|
|
|
|
// Create a new one
|
|
return CreateRenderTarget(Size);
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::ReleaseRenderTarget(UTextureRenderTarget2D* RenderTarget)
|
|
{
|
|
if (!RenderTarget)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Find the entry and mark as free
|
|
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
|
{
|
|
if (Entry.RenderTarget == RenderTarget)
|
|
{
|
|
Entry.bInUse = false;
|
|
Entry.OwningSurface.Reset();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Not in pool — add it
|
|
FPlanarCaptureRenderTargetEntry NewEntry;
|
|
NewEntry.RenderTarget = RenderTarget;
|
|
NewEntry.CurrentSize = RenderTarget->SizeX;
|
|
NewEntry.bInUse = false;
|
|
RenderTargetPool.Add(NewEntry);
|
|
}
|
|
|
|
float ASS_PlanarCaptureManager::GetPoolMemoryUsageMB() const
|
|
{
|
|
float TotalBytes = 0.0f;
|
|
for (const FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
|
{
|
|
if (Entry.RenderTarget)
|
|
{
|
|
// RGBA8 = 4 bytes per pixel
|
|
TotalBytes += static_cast<float>(Entry.CurrentSize * Entry.CurrentSize * 4);
|
|
}
|
|
}
|
|
return TotalBytes / (1024.0f * 1024.0f);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Query
|
|
// ========================================================================
|
|
|
|
ABP_PlanarCaptureActor* ASS_PlanarCaptureManager::GetNearestSurfaceOfMode(
|
|
EPlanarCaptureMode Mode, FVector WorldLocation, float MaxDistance) const
|
|
{
|
|
ABP_PlanarCaptureActor* Nearest = nullptr;
|
|
float NearestDistSq = (MaxDistance > 0.0f) ? (MaxDistance * MaxDistance) : FLT_MAX;
|
|
|
|
for (const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry : RegisteredSurfaces)
|
|
{
|
|
if (ABP_PlanarCaptureActor* Surface = Entry.Get())
|
|
{
|
|
if (!Surface->CaptureComponent || Surface->CaptureComponent->CaptureMode != Mode)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const float DistSq = FVector::DistSquared(Surface->GetActorLocation(), WorldLocation);
|
|
if (DistSq < NearestDistSq)
|
|
{
|
|
NearestDistSq = DistSq;
|
|
Nearest = Surface;
|
|
}
|
|
}
|
|
}
|
|
|
|
return Nearest;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Internal Methods
|
|
// ========================================================================
|
|
|
|
void ASS_PlanarCaptureManager::EvaluateAllSurfaces()
|
|
{
|
|
// Reset tier counts
|
|
TierAssignmentCounts.Empty();
|
|
|
|
// Ensure registered surfaces are still valid
|
|
RegisteredSurfaces.RemoveAll([](const TWeakObjectPtr<ABP_PlanarCaptureActor>& Entry)
|
|
{
|
|
return !Entry.IsValid();
|
|
});
|
|
|
|
// If force tier override is active, apply it to all
|
|
if (ForceTierOverride.IsSet())
|
|
{
|
|
ForceAllSurfacesToTier(ForceTierOverride.GetValue());
|
|
return;
|
|
}
|
|
|
|
// Score and assign tiers
|
|
TArray<TPair<ABP_PlanarCaptureActor*, float>> ScoredSurfaces;
|
|
|
|
for (TWeakObjectPtr<ABP_PlanarCaptureActor>& SurfaceWeak : RegisteredSurfaces)
|
|
{
|
|
if (ABP_PlanarCaptureActor* Surface = SurfaceWeak.Get())
|
|
{
|
|
if (!Surface->bIsActive || !Surface->CaptureComponent)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
UBPC_PlanarCapture* Capture = Surface->CaptureComponent;
|
|
FPlanarCaptureScore Score = Capture->GetCurrentScore();
|
|
ScoredSurfaces.Add(TPair<ABP_PlanarCaptureActor*, float>(Surface, Score.CompositeScore));
|
|
}
|
|
}
|
|
|
|
// Sort by score descending (highest score first)
|
|
ScoredSurfaces.Sort([](const TPair<ABP_PlanarCaptureActor*, float>& A,
|
|
const TPair<ABP_PlanarCaptureActor*, float>& B)
|
|
{
|
|
return A.Value > B.Value;
|
|
});
|
|
|
|
// Assign tiers within budget
|
|
for (const auto& Pair : ScoredSurfaces)
|
|
{
|
|
ABP_PlanarCaptureActor* Surface = Pair.Key;
|
|
float Score = Pair.Value;
|
|
|
|
if (!Surface->CaptureComponent)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EPlanarCaptureQualityTier AssignedTier = ScoreAndAssignTier(Surface->CaptureComponent);
|
|
|
|
// Apply tier
|
|
Surface->CaptureComponent->ApplyQualityTier(AssignedTier);
|
|
}
|
|
}
|
|
|
|
EPlanarCaptureQualityTier ASS_PlanarCaptureManager::ScoreAndAssignTier(UBPC_PlanarCapture* Capture)
|
|
{
|
|
if (!Capture)
|
|
{
|
|
return EPlanarCaptureQualityTier::Off;
|
|
}
|
|
|
|
const FPlanarCaptureScore Score = Capture->GetCurrentScore();
|
|
|
|
// If not visible, off
|
|
if (!Score.bInFrustum && Score.CompositeScore < 0.01f)
|
|
{
|
|
return EPlanarCaptureQualityTier::Off;
|
|
}
|
|
|
|
// If too far, off
|
|
if (Score.DistanceToViewer > MaxCaptureDistance)
|
|
{
|
|
return EPlanarCaptureQualityTier::Off;
|
|
}
|
|
|
|
// Determine natural tier based on composite score
|
|
EPlanarCaptureQualityTier NaturalTier;
|
|
|
|
if (Score.CompositeScore >= 0.8f)
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::Hero;
|
|
}
|
|
else if (Score.CompositeScore >= 0.5f)
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::High;
|
|
}
|
|
else if (Score.CompositeScore >= 0.2f)
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::Medium;
|
|
}
|
|
else
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::Low;
|
|
}
|
|
|
|
// Apply global quality cap
|
|
if (static_cast<int32>(NaturalTier) > static_cast<int32>(GlobalQualityCap))
|
|
{
|
|
NaturalTier = GlobalQualityCap;
|
|
}
|
|
|
|
// Check budget limits
|
|
const int32 CurrentCount = TierAssignmentCounts.FindRef(NaturalTier);
|
|
|
|
switch (NaturalTier)
|
|
{
|
|
case EPlanarCaptureQualityTier::Hero:
|
|
if (CurrentCount >= MaxHeroSurfaces)
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::High;
|
|
}
|
|
break;
|
|
case EPlanarCaptureQualityTier::High:
|
|
if (CurrentCount >= MaxHighSurfaces)
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::Medium;
|
|
}
|
|
break;
|
|
case EPlanarCaptureQualityTier::Medium:
|
|
if (CurrentCount >= MaxMediumSurfaces)
|
|
{
|
|
NaturalTier = EPlanarCaptureQualityTier::Low;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Increment the assigned tier count
|
|
TierAssignmentCounts.FindOrAdd(NaturalTier)++;
|
|
|
|
return NaturalTier;
|
|
}
|
|
|
|
void ASS_PlanarCaptureManager::EnforceBudgetLimits()
|
|
{
|
|
// Additional enforcement for total memory budget
|
|
float TotalMemoryMB = GetPoolMemoryUsageMB();
|
|
|
|
if (TotalMemoryMB > MaxTotalRenderTargetMemoryMB)
|
|
{
|
|
UE_LOG(LogTemp, Warning, TEXT("SS_PlanarCaptureManager: Render target memory budget exceeded (%.2f MB / %.2f MB)"),
|
|
TotalMemoryMB, MaxTotalRenderTargetMemoryMB);
|
|
|
|
// If over budget, demote lowest-priority surfaces
|
|
// Future: implement more sophisticated memory budget enforcement
|
|
}
|
|
}
|
|
|
|
UTextureRenderTarget2D* ASS_PlanarCaptureManager::CreateRenderTarget(int32 Size)
|
|
{
|
|
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>(this);
|
|
if (!RT)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
RT->InitCustomFormat(Size, Size, PF_B8G8R8A8, false);
|
|
RT->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8;
|
|
RT->bGPUSharedFlag = false;
|
|
RT->ClearColor = FLinearColor::Black;
|
|
RT->UpdateResourceImmediate(true);
|
|
|
|
FPlanarCaptureRenderTargetEntry Entry;
|
|
Entry.RenderTarget = RT;
|
|
Entry.CurrentSize = Size;
|
|
Entry.bInUse = true;
|
|
RenderTargetPool.Add(Entry);
|
|
|
|
return RT;
|
|
}
|
|
|
|
UTextureRenderTarget2D* ASS_PlanarCaptureManager::GetOrCreateRenderTarget(int32 Size)
|
|
{
|
|
// Check pool first
|
|
for (FPlanarCaptureRenderTargetEntry& Entry : RenderTargetPool)
|
|
{
|
|
if (!Entry.bInUse && Entry.CurrentSize == Size && Entry.RenderTarget)
|
|
{
|
|
Entry.bInUse = true;
|
|
return Entry.RenderTarget;
|
|
}
|
|
}
|
|
|
|
return CreateRenderTarget(Size);
|
|
}
|