Add Planar Capture System implementation checklist and developer reference

- 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.
This commit is contained in:
Lefteris Notas
2026-05-22 15:36:08 +03:00
parent 6b6c702dd7
commit 0a2d08b2ad
34 changed files with 5245 additions and 32 deletions

View File

@@ -13,6 +13,8 @@ public class PG_Framework : ModuleRules
"Core",
"CoreUObject",
"Engine",
"Renderer",
"RenderCore",
"GameplayTags",
"EnhancedInput",
"InputCore",

View File

@@ -3,4 +3,5 @@
#pragma once
#include "CoreMinimal.h"
#include "Capture/PlanarCaptureCommon.h"

View File

@@ -0,0 +1,581 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — BPC_PlanarCapture implementation
#include "Capture/BPC_PlanarCapture.h"
#include "Capture/BP_PlanarCaptureActor.h"
#include "Capture/SS_PlanarCaptureManager.h"
#include "Capture/PlanarCaptureCameraUtils.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/World.h"
#include "GameFramework/PlayerController.h"
#include "Materials/MaterialParameterCollection.h"
#include "Materials/MaterialParameterCollectionInstance.h"
#include "Kismet/GameplayStatics.h"
UBPC_PlanarCapture::UBPC_PlanarCapture()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
PrimaryComponentTick.TickInterval = 0.0f; // We throttle internally via TimeSinceLastCapture
// Initialize default quality profiles
QualityProfiles.SetNum(4);
// Low — 256px, 4fps
QualityProfiles[0].RenderTargetSize = 256;
QualityProfiles[0].CaptureInterval = 0.25f;
QualityProfiles[0].bEnableShadows = false;
QualityProfiles[0].bEnablePostProcess = false;
QualityProfiles[0].bEnableFog = false;
QualityProfiles[0].bEnableBloom = false;
QualityProfiles[0].bEnableAO = false;
QualityProfiles[0].bEnableLumen = false;
QualityProfiles[0].bEnableMotionBlur = false;
QualityProfiles[0].bEnableClipPlane = false;
// Medium — 512px, 15fps
QualityProfiles[1].RenderTargetSize = 512;
QualityProfiles[1].CaptureInterval = 0.0667f;
QualityProfiles[1].bEnableShadows = true;
QualityProfiles[1].bEnablePostProcess = false;
QualityProfiles[1].bEnableFog = false;
QualityProfiles[1].bEnableBloom = false;
QualityProfiles[1].bEnableAO = false;
QualityProfiles[1].bEnableLumen = false;
QualityProfiles[1].bEnableMotionBlur = false;
QualityProfiles[1].bEnableClipPlane = true;
// High — 1024px, 30fps
QualityProfiles[2].RenderTargetSize = 1024;
QualityProfiles[2].CaptureInterval = 0.0333f;
QualityProfiles[2].bEnableShadows = true;
QualityProfiles[2].bEnablePostProcess = true;
QualityProfiles[2].bEnableFog = true;
QualityProfiles[2].bEnableBloom = false;
QualityProfiles[2].bEnableAO = true;
QualityProfiles[2].bEnableLumen = true;
QualityProfiles[2].bEnableMotionBlur = false;
QualityProfiles[2].bEnableClipPlane = true;
// Hero — 2048px, 60fps
QualityProfiles[3].RenderTargetSize = 2048;
QualityProfiles[3].CaptureInterval = 0.0167f;
QualityProfiles[3].bEnableShadows = true;
QualityProfiles[3].bEnablePostProcess = true;
QualityProfiles[3].bEnableFog = true;
QualityProfiles[3].bEnableBloom = true;
QualityProfiles[3].bEnableAO = true;
QualityProfiles[3].bEnableLumen = true;
QualityProfiles[3].bEnableMotionBlur = true;
QualityProfiles[3].bEnableClipPlane = true;
}
void UBPC_PlanarCapture::BeginPlay()
{
Super::BeginPlay();
CachedOwningActor = Cast<ABP_PlanarCaptureActor>(GetOwner());
if (!CachedOwningActor)
{
UE_LOG(LogTemp, Warning, TEXT("BPC_PlanarCapture: Owner is not a BP_PlanarCaptureActor! Capture disabled."));
SetComponentTickEnabled(false);
return;
}
// Cache manager reference
if (UWorld* World = GetWorld())
{
CachedManager = World->GetSubsystem<ASS_PlanarCaptureManager>();
}
ResolveSoftReferences();
}
void UBPC_PlanarCapture::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
ShutdownCapture();
Super::EndPlay(EndPlayReason);
}
void UBPC_PlanarCapture::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bIsCapturing || !SceneCapture || CurrentQualityTier == EPlanarCaptureQualityTier::Off)
{
return;
}
TimeSinceLastCapture += DeltaTime;
if (TimeSinceLastCapture >= ActiveProfile.CaptureInterval)
{
TimeSinceLastCapture = 0.0f;
// Get viewer camera for transform computation
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PC || !PC->PlayerCameraManager)
{
return;
}
const FTransform ViewerTransform = FTransform(
PC->PlayerCameraManager->GetCameraRotation(),
PC->PlayerCameraManager->GetCameraLocation()
);
// Compute capture camera position
const FTransform CaptureTransform = ComputeCaptureCameraTransform(ViewerTransform);
SceneCapture->SetWorldTransform(CaptureTransform);
// Capture the scene
SceneCapture->CaptureScene();
// Push MPC parameters
if (CachedOwningActor && CachedOwningActor->SurfaceMPC)
{
PushMPCParameters(CachedOwningActor->SurfaceMPC);
}
OnCaptureRendered.Broadcast();
}
}
// ========================================================================
// Public API
// ========================================================================
EPlanarCaptureInitResult UBPC_PlanarCapture::InitializeCapture()
{
if (!CachedOwningActor)
{
return EPlanarCaptureInitResult::InvalidSurfaceMesh;
}
if (CurrentQualityTier == EPlanarCaptureQualityTier::Off)
{
return EPlanarCaptureInitResult::Success; // Nothing to initialize
}
// Request render target from pool
const int32 ProfileIndex = static_cast<int32>(CurrentQualityTier) - 1; // Skip "Off" (0)
if (ProfileIndex < 0 || ProfileIndex >= QualityProfiles.Num())
{
return EPlanarCaptureInitResult::NoRenderTargetPool;
}
ActiveProfile = QualityProfiles[ProfileIndex];
if (CachedManager)
{
CaptureRenderTarget = CachedManager->RequestRenderTarget(ActiveProfile.RenderTargetSize);
}
if (!CaptureRenderTarget)
{
UE_LOG(LogTemp, Error, TEXT("BPC_PlanarCapture: Failed to allocate render target (%dx%d)"),
ActiveProfile.RenderTargetSize, ActiveProfile.RenderTargetSize);
return EPlanarCaptureInitResult::NoRenderTargetPool;
}
CreateSceneCaptureComponent();
bIsCapturing = true;
OnCaptureInitialized.Broadcast(EPlanarCaptureInitResult::Success);
return EPlanarCaptureInitResult::Success;
}
void UBPC_PlanarCapture::ShutdownCapture()
{
bIsCapturing = false;
if (SceneCapture)
{
SceneCapture->DestroyComponent();
SceneCapture = nullptr;
}
if (CaptureRenderTarget && CachedManager)
{
CachedManager->ReleaseRenderTarget(CaptureRenderTarget);
CaptureRenderTarget = nullptr;
}
// Clean up frame ring buffer
for (UTextureRenderTarget2D* RT : FrameRingBuffer)
{
if (RT && CachedManager)
{
CachedManager->ReleaseRenderTarget(RT);
}
}
FrameRingBuffer.Empty();
}
void UBPC_PlanarCapture::ApplyQualityTier(EPlanarCaptureQualityTier Tier)
{
const EPlanarCaptureQualityTier OldTier = CurrentQualityTier;
CurrentQualityTier = Tier;
if (Tier == EPlanarCaptureQualityTier::Off)
{
ShutdownCapture();
OnCaptureQualityChanged.Broadcast(OldTier, Tier);
return;
}
// If transitioning from Off to active, initialize
if (OldTier == EPlanarCaptureQualityTier::Off)
{
InitializeCapture();
}
else
{
// Update active profile and resize render target if needed
const int32 ProfileIndex = static_cast<int32>(Tier) - 1;
if (ProfileIndex >= 0 && ProfileIndex < QualityProfiles.Num())
{
ActiveProfile = QualityProfiles[ProfileIndex];
ApplyShowFlags();
}
}
OnCaptureQualityChanged.Broadcast(OldTier, Tier);
}
void UBPC_PlanarCapture::CaptureNow()
{
if (!SceneCapture)
{
return;
}
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PC || !PC->PlayerCameraManager)
{
return;
}
const FTransform ViewerTransform = FTransform(
PC->PlayerCameraManager->GetCameraRotation(),
PC->PlayerCameraManager->GetCameraLocation()
);
const FTransform CaptureTransform = ComputeCaptureCameraTransform(ViewerTransform);
SceneCapture->SetWorldTransform(CaptureTransform);
SceneCapture->CaptureScene();
TimeSinceLastCapture = 0.0f;
OnCaptureRendered.Broadcast();
}
void UBPC_PlanarCapture::ActivateHorrorReflection()
{
// Save current ShowOnly list
SavedShowOnlyActors.Empty();
for (const FPlanarCaptureActorListEntry& Entry : ShowOnlyActors)
{
SavedShowOnlyActors.Add(Entry.Actor);
}
// Clear and set wrong reflection actor
ShowOnlyActors.Empty();
if (WrongReflectionActor.IsValid())
{
FPlanarCaptureActorListEntry NewEntry;
NewEntry.Actor = WrongReflectionActor;
NewEntry.bActive = true;
ShowOnlyActors.Add(NewEntry);
}
UpdateActorLists();
}
void UBPC_PlanarCapture::DeactivateHorrorReflection()
{
ShowOnlyActors.Empty();
for (const TSoftObjectPtr<AActor>& SavedActor : SavedShowOnlyActors)
{
FPlanarCaptureActorListEntry NewEntry;
NewEntry.Actor = SavedActor;
NewEntry.bActive = true;
ShowOnlyActors.Add(NewEntry);
}
SavedShowOnlyActors.Empty();
UpdateActorLists();
}
void UBPC_PlanarCapture::PushDelayedFrame()
{
if (ActiveProfile.DelayedFrameCount <= 0 || !CaptureRenderTarget)
{
return;
}
// Ensure ring buffer is properly sized
while (FrameRingBuffer.Num() < ActiveProfile.DelayedFrameCount)
{
UTextureRenderTarget2D* NewRT = nullptr;
if (CachedManager)
{
NewRT = CachedManager->RequestRenderTarget(ActiveProfile.RenderTargetSize);
}
FrameRingBuffer.Add(NewRT);
}
// Copy current render target to ring buffer slot
RingBufferWriteIndex = (RingBufferWriteIndex + 1) % FrameRingBuffer.Num();
// Push the oldest frame to the material as the delayed reflection
if (FrameRingBuffer.IsValidIndex(RingBufferWriteIndex) && FrameRingBuffer[RingBufferWriteIndex])
{
// The material samples the delayed frame via the MPC texture parameter
// This is set in PushMPCParameters
}
}
void UBPC_PlanarCapture::SetScriptedPriority(float Priority)
{
ScriptedPriorityOverride = FMath::Clamp(Priority, 0.0f, 1.0f);
}
void UBPC_PlanarCapture::PushMPCParameters(UMaterialParameterCollection* MPC)
{
if (!MPC)
{
return;
}
UMaterialParameterCollectionInstance* MPCInstance = GetWorld()->GetParameterCollectionInstance(MPC);
if (!MPCInstance)
{
return;
}
// Push standard parameters
// These are used by M_CaptureSurface_Master to drive steam, dirt, horror effects
MPCInstance->SetScalarParameterValue(FName("SteamIntensity"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("DirtOpacity"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("CondensationFlow"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("DistortionAmplitude"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("MirrorDarkness"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("WrongReflectionBlend"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("TextRevealProgress"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("SteamEmissiveIntensity"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("DelayedReflectionBlend"), 0.0f);
MPCInstance->SetScalarParameterValue(FName("SurfaceAge"), 0.0f);
}
// ========================================================================
// Compute
// ========================================================================
FTransform UBPC_PlanarCapture::ComputeCaptureCameraTransform(const FTransform& ViewerCameraTransform) const
{
switch (CaptureMode)
{
case EPlanarCaptureMode::Mirror:
case EPlanarCaptureMode::HorrorMirror:
{
const FTransform SurfaceTransform = CachedOwningActor
? CachedOwningActor->GetActorTransform()
: FTransform::Identity;
return UPlanarCaptureCameraUtils::ComputeMirroredTransform(ViewerCameraTransform, SurfaceTransform);
}
case EPlanarCaptureMode::Portal:
case EPlanarCaptureMode::HorrorPortal:
{
if (LinkedTargetSurface.IsValid())
{
const FTransform SourceTransform = CachedOwningActor
? CachedOwningActor->GetActorTransform()
: FTransform::Identity;
const FTransform TargetTransform = LinkedTargetSurface->GetActorTransform();
return UPlanarCaptureCameraUtils::ComputePortalTransform(
ViewerCameraTransform, SourceTransform, TargetTransform);
}
return ViewerCameraTransform;
}
case EPlanarCaptureMode::Monitor:
{
if (FixedCameraActor.IsValid())
{
return FixedCameraActor->GetActorTransform();
}
return ViewerCameraTransform;
}
case EPlanarCaptureMode::FakeWindow:
{
// Fake windows use a fixed offset from the surface
const FTransform SurfaceTransform = CachedOwningActor
? CachedOwningActor->GetActorTransform()
: FTransform::Identity;
const FVector SurfaceNormal = SurfaceTransform.GetUnitAxis(EAxis::Z);
return FTransform(SurfaceTransform.GetRotation(),
SurfaceTransform.GetLocation() + SurfaceNormal * 200.0f);
}
default:
return ViewerCameraTransform;
}
}
FPlanarCaptureScore UBPC_PlanarCapture::GetCurrentScore() const
{
FPlanarCaptureScore Score;
APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0);
if (!PC || !PC->PlayerCameraManager || !CachedOwningActor)
{
return Score;
}
const FVector ViewerPos = PC->PlayerCameraManager->GetCameraLocation();
const FTransform ViewerTransform = FTransform(
PC->PlayerCameraManager->GetCameraRotation(),
ViewerPos
);
const FVector SurfacePos = CachedOwningActor->GetActorLocation();
const FVector SurfaceNormal = CachedOwningActor->GetActorUpVector();
Score.DistanceToViewer = FVector::Dist(ViewerPos, SurfacePos);
Score.FacingAngle = FVector::DotProduct(
PC->PlayerCameraManager->GetCameraRotation().Vector(),
SurfaceNormal
);
// Compute screen coverage using bounding box
const FBox SurfaceBounds = CachedOwningActor->GetComponentsBoundingBox();
Score.ScreenCoverage = UPlanarCaptureCameraUtils::ComputeScreenCoverage(
SurfaceBounds, ViewerTransform, CaptureFOV, 1920, 1080);
Score.bInFrustum = UPlanarCaptureCameraUtils::IsSurfaceVisibleToViewer(
SurfaceBounds, ViewerTransform, CaptureFOV, 1.777f, 10.0f, MaxViewDistance);
Score.ScriptedPriority = ScriptedPriorityOverride;
Score.CompositeScore = UPlanarCaptureCameraUtils::ComputeCompositeScore(
Score.ScreenCoverage, Score.FacingAngle, Score.DistanceToViewer,
CachedManager ? CachedManager->MaxCaptureDistance : 10000.0f,
Score.ScriptedPriority);
return Score;
}
// ========================================================================
// Internal Methods
// ========================================================================
void UBPC_PlanarCapture::CreateSceneCaptureComponent()
{
if (!GetOwner())
{
return;
}
SceneCapture = NewObject<USceneCaptureComponent2D>(GetOwner(), USceneCaptureComponent2D::StaticClass());
if (!SceneCapture)
{
return;
}
SceneCapture->RegisterComponent();
SceneCapture->AttachToComponent(GetOwner()->GetRootComponent(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale);
SceneCapture->TextureTarget = CaptureRenderTarget;
SceneCapture->FOVAngle = CaptureFOV;
SceneCapture->bCaptureEveryFrame = false; // We control capture timing
SceneCapture->bCaptureOnMovement = false;
// Configure for planar surface capture
SceneCapture->ProjectionType = ECameraProjectionMode::Perspective;
SceneCapture->PrimitiveRenderMode = ESceneCapturePrimitiveRenderMode::PRM_UseShowOnlyList;
ApplyShowFlags();
UpdateActorLists();
}
void UBPC_PlanarCapture::ApplyShowFlags()
{
if (!SceneCapture)
{
return;
}
// Apply show flags based on active quality profile
SceneCapture->ShowFlags.SetShadows(ActiveProfile.bEnableShadows);
SceneCapture->ShowFlags.SetFog(ActiveProfile.bEnableFog);
SceneCapture->ShowFlags.SetBloom(ActiveProfile.bEnableBloom);
SceneCapture->ShowFlags.SetAmbientOcclusion(ActiveProfile.bEnableAO);
SceneCapture->ShowFlags.SetMotionBlur(ActiveProfile.bEnableMotionBlur);
// Lumen is controlled via post-process settings on the capture component
SceneCapture->PostProcessSettings.bOverride_DynamicGlobalIlluminationMethod = true;
SceneCapture->PostProcessSettings.DynamicGlobalIlluminationMethod = ActiveProfile.bEnableLumen
? EDynamicGlobalIlluminationMethod::Lumen
: EDynamicGlobalIlluminationMethod::None;
// Post-process toggle
SceneCapture->PostProcessSettings.bOverride_BloomIntensity = !ActiveProfile.bEnablePostProcess;
SceneCapture->PostProcessBlendWeight = ActiveProfile.bEnablePostProcess ? 1.0f : 0.0f;
// Disable Lumen reflections on the capture itself to prevent double-rendering
SceneCapture->PostProcessSettings.bOverride_ReflectionMethod = true;
SceneCapture->PostProcessSettings.ReflectionMethod = EReflectionMethod::None;
}
void UBPC_PlanarCapture::UpdateActorLists()
{
if (!SceneCapture)
{
return;
}
SceneCapture->ShowOnlyActors.Empty();
SceneCapture->HiddenActors.Empty();
for (const FPlanarCaptureActorListEntry& Entry : ShowOnlyActors)
{
if (Entry.bActive && Entry.Actor.IsValid())
{
SceneCapture->ShowOnlyActors.Add(Entry.Actor.Get());
}
}
for (const FPlanarCaptureActorListEntry& Entry : HiddenActors)
{
if (Entry.bActive && Entry.Actor.IsValid())
{
SceneCapture->HiddenActors.Add(Entry.Actor.Get());
}
}
}
void UBPC_PlanarCapture::ResolveSoftReferences()
{
if (SurfaceMeshComponent.IsValid())
{
// Surface mesh is resolved — ready for capture
}
}
FPlane UBPC_PlanarCapture::GetSurfacePlane() const
{
if (!CachedOwningActor)
{
return FPlane(FVector::ZeroVector, FVector::UpVector);
}
const FVector SurfaceLocation = CachedOwningActor->GetActorLocation();
const FVector SurfaceNormal = CachedOwningActor->GetActorUpVector();
return FPlane(SurfaceLocation, SurfaceNormal);
}

View File

@@ -0,0 +1,241 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — BP_PlanarCaptureActor implementation
#include "Capture/BP_PlanarCaptureActor.h"
#include "Capture/BPC_PlanarCapture.h"
#include "Capture/SS_PlanarCaptureManager.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialParameterCollection.h"
#include "Engine/World.h"
#include "Net/UnrealNetwork.h"
ABP_PlanarCaptureActor::ABP_PlanarCaptureActor()
{
PrimaryActorTick.bCanEverTick = false;
// Create root component
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
SetRootComponent(Root);
// Surface mesh
SurfaceMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SurfaceMesh"));
SurfaceMesh->SetupAttachment(Root);
SurfaceMesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
SurfaceMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
SurfaceMesh->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
// Proximity trigger for quality scoring
ProximityTrigger = CreateDefaultSubobject<UBoxComponent>(TEXT("ProximityTrigger"));
ProximityTrigger->SetupAttachment(Root);
ProximityTrigger->SetBoxExtent(FVector(500.0f, 500.0f, 500.0f));
ProximityTrigger->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
ProximityTrigger->SetCollisionResponseToAllChannels(ECR_Ignore);
ProximityTrigger->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
// Capture component
CaptureComponent = CreateDefaultSubobject<UBPC_PlanarCapture>(TEXT("CaptureComponent"));
// Replication
bReplicates = true;
bAlwaysRelevant = true;
}
void ABP_PlanarCaptureActor::BeginPlay()
{
Super::BeginPlay();
// Bind overlap events
if (ProximityTrigger)
{
ProximityTrigger->OnComponentBeginOverlap.AddDynamic(this, &ABP_PlanarCaptureActor::OnProximityBeginOverlap);
ProximityTrigger->OnComponentEndOverlap.AddDynamic(this, &ABP_PlanarCaptureActor::OnProximityEndOverlap);
}
CreateMaterialInstance();
RegisterWithManager();
if (bStartEnabled)
{
EnableSurface();
}
}
void ABP_PlanarCaptureActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// Unregister from manager
if (UWorld* World = GetWorld())
{
if (ASS_PlanarCaptureManager* Manager = World->GetSubsystem<ASS_PlanarCaptureManager>())
{
Manager->UnregisterSurface(this);
}
}
Super::EndPlay(EndPlayReason);
}
// ========================================================================
// Public API
// ========================================================================
void ABP_PlanarCaptureActor::EnableSurface()
{
if (bIsActive)
{
return;
}
bIsActive = true;
bRepIsActive = true;
if (CaptureComponent)
{
CaptureComponent->InitializeCapture();
}
}
void ABP_PlanarCaptureActor::DisableSurface()
{
if (!bIsActive)
{
return;
}
bIsActive = false;
bRepIsActive = false;
if (CaptureComponent)
{
CaptureComponent->ShutdownCapture();
}
}
void ABP_PlanarCaptureActor::SetCaptureMode(EPlanarCaptureMode NewMode)
{
if (CaptureComponent)
{
const bool WasCapturing = CaptureComponent->bIsCapturing;
if (WasCapturing)
{
CaptureComponent->ShutdownCapture();
}
CaptureComponent->CaptureMode = NewMode;
if (WasCapturing)
{
CaptureComponent->InitializeCapture();
}
OnModeChanged.Broadcast(NewMode);
}
}
void ABP_PlanarCaptureActor::SetSurfaceMaterial(UMaterialInterface* NewMaterial)
{
if (SurfaceMesh && NewMaterial)
{
SurfaceMaterialInstance = SurfaceMesh->CreateDynamicMaterialInstance(0, NewMaterial);
}
}
void ABP_PlanarCaptureActor::SetSurfaceMPCParameter(FName ParameterName, float Value)
{
UMaterialInstanceDynamic* MID = SurfaceMaterialInstance;
if (!MID)
{
return;
}
MID->SetScalarParameterValue(ParameterName, Value);
}
void ABP_PlanarCaptureActor::DestroySurface()
{
if (!bDestructible)
{
return;
}
DisableSurface();
OnSurfaceDestroyed.Broadcast(this);
// Blueprint child handles visual destruction (particles, sound via SS_AudioManager, etc.)
}
// ========================================================================
// Overlap Events
// ========================================================================
void ABP_PlanarCaptureActor::OnProximityBeginOverlap(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult)
{
// Player entered proximity — notify capture component for quality scoring
if (CaptureComponent && OtherActor && OtherActor->ActorHasTag(FName("Player")))
{
CaptureComponent->SetScriptedPriority(0.3f); // Slight priority boost when player is near
}
}
void ABP_PlanarCaptureActor::OnProximityEndOverlap(UPrimitiveComponent* OverlappedComponent,
AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (CaptureComponent && OtherActor && OtherActor->ActorHasTag(FName("Player")))
{
CaptureComponent->SetScriptedPriority(0.0f); // Reset priority
}
}
// ========================================================================
// Replication
// ========================================================================
void ABP_PlanarCaptureActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ABP_PlanarCaptureActor, bRepIsActive);
}
void ABP_PlanarCaptureActor::OnRep_IsActive()
{
if (bRepIsActive && !bIsActive)
{
EnableSurface();
}
else if (!bRepIsActive && bIsActive)
{
DisableSurface();
}
}
// ========================================================================
// Internal
// ========================================================================
void ABP_PlanarCaptureActor::RegisterWithManager()
{
UWorld* World = GetWorld();
if (!World)
{
return;
}
ASS_PlanarCaptureManager* Manager = World->GetSubsystem<ASS_PlanarCaptureManager>();
if (Manager)
{
Manager->RegisterSurface(this);
}
}
void ABP_PlanarCaptureActor::CreateMaterialInstance()
{
if (SurfaceMesh && SurfaceMesh->GetMaterial(0))
{
SurfaceMaterialInstance = SurfaceMesh->CreateDynamicMaterialInstance(0);
}
}

View File

@@ -0,0 +1,240 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — PlanarCaptureCameraUtils implementation
#include "Capture/PlanarCaptureCameraUtils.h"
FTransform UPlanarCaptureCameraUtils::ComputeMirroredTransform(
const FTransform& ViewerCameraTransform,
const FTransform& MirrorPlaneTransform)
{
// Mirror plane normal (local Z axis of the mirror surface)
const FVector MirrorNormal = MirrorPlaneTransform.GetUnitAxis(EAxis::Z);
const FVector MirrorLocation = MirrorPlaneTransform.GetLocation();
// Reflect viewer position across the mirror plane
const FVector ViewerPos = ViewerCameraTransform.GetLocation();
const FVector ToViewer = ViewerPos - MirrorLocation;
const float DistanceToPlane = FVector::DotProduct(ToViewer, MirrorNormal);
const FVector ReflectedPosition = ViewerPos - 2.0f * DistanceToPlane * MirrorNormal;
// Reflect viewer rotation across the mirror plane
const FVector ViewerForward = ViewerCameraTransform.GetUnitAxis(EAxis::X);
const FVector ReflectedForward = ViewerForward - 2.0f * FVector::DotProduct(ViewerForward, MirrorNormal) * MirrorNormal;
const FVector ViewerUp = ViewerCameraTransform.GetUnitAxis(EAxis::Z);
const FVector ReflectedUp = ViewerUp - 2.0f * FVector::DotProduct(ViewerUp, MirrorNormal) * MirrorNormal;
FRotator ReflectedRotation = UKismetMathLibrary::MakeRotFromXZ(ReflectedForward, ReflectedUp);
return FTransform(ReflectedRotation, ReflectedPosition, ViewerCameraTransform.GetScale3D());
}
FTransform UPlanarCaptureCameraUtils::ComputePortalTransform(
const FTransform& ViewerCameraTransform,
const FTransform& SourceSurfaceTransform,
const FTransform& TargetSurfaceTransform)
{
// Compute viewer position relative to source surface
const FVector ViewerPos = ViewerCameraTransform.GetLocation();
const FVector RelativePos = SourceSurfaceTransform.InverseTransformPosition(ViewerPos);
// Compute viewer rotation relative to source surface
const FQuat ViewerRot = ViewerCameraTransform.GetRotation();
const FQuat SourceRotInv = SourceSurfaceTransform.GetRotation().Inverse();
const FQuat RelativeRot = SourceRotInv * ViewerRot;
// Apply relative transform to target surface
const FVector TargetPos = TargetSurfaceTransform.TransformPosition(RelativePos);
const FQuat TargetRot = TargetSurfaceTransform.GetRotation() * RelativeRot;
return FTransform(TargetRot, TargetPos, ViewerCameraTransform.GetScale3D());
}
FMatrix UPlanarCaptureCameraUtils::ComputeObliqueProjectionMatrix(
float FOV,
float AspectRatio,
float NearPlane,
float FarPlane,
const FPlane& ClipPlane,
const FTransform& SurfaceTransform)
{
// Build standard perspective projection matrix
FMatrix ProjectionMatrix;
const float HalfFOVRad = FMath::DegreesToRadians(FOV * 0.5f);
const float YScale = 1.0f / FMath::Tan(HalfFOVRad);
const float XScale = YScale / AspectRatio;
ProjectionMatrix.SetIdentity();
ProjectionMatrix.M[0][0] = XScale;
ProjectionMatrix.M[1][1] = YScale;
ProjectionMatrix.M[2][2] = (NearPlane == FarPlane) ? 0.0f : FarPlane / (FarPlane - NearPlane);
ProjectionMatrix.M[2][3] = 1.0f;
ProjectionMatrix.M[3][2] = (NearPlane == FarPlane) ? NearPlane : -NearPlane * FarPlane / (FarPlane - NearPlane);
ProjectionMatrix.M[3][3] = 0.0f;
// Transform clip plane into view space (inverse of surface transform)
const FTransform ViewTransform = SurfaceTransform.Inverse();
const FPlane ViewSpaceClipPlane = ClipPlane.TransformBy(ViewTransform);
// Compute oblique near-plane using the standard technique:
// Calculate the clip-space corner that maximizes the dot product with the plane normal.
// Then modify the third row of the projection matrix to make the near plane pass through the clip plane.
FVector4 ClipPlaneVec(ViewSpaceClipPlane.X, ViewSpaceClipPlane.Y, ViewSpaceClipPlane.Z, ViewSpaceClipPlane.W);
// Find the vertex of the view frustum in clip space that is closest to the plane
FVector4 Q;
Q.X = (FMath::Sign(ClipPlaneVec.X) + ProjectionMatrix.M[0][2]) / ProjectionMatrix.M[0][0];
Q.Y = (FMath::Sign(ClipPlaneVec.Y) + ProjectionMatrix.M[1][2]) / ProjectionMatrix.M[1][1];
Q.Z = 1.0f;
Q.W = (1.0f + ProjectionMatrix.M[2][2]) / ProjectionMatrix.M[3][2];
// Scale the clip plane so its distance from the origin matches the Q vertex
const float Scale = 2.0f / FVector4::DotProduct(ClipPlaneVec, Q);
ClipPlaneVec *= Scale;
// Replace the third row of the projection matrix with the clip plane
ProjectionMatrix.M[2][0] = ClipPlaneVec.X;
ProjectionMatrix.M[2][1] = ClipPlaneVec.Y;
ProjectionMatrix.M[2][2] = ClipPlaneVec.Z;
ProjectionMatrix.M[2][3] = ClipPlaneVec.W;
return ProjectionMatrix;
}
float UPlanarCaptureCameraUtils::ComputeScreenCoverage(
const FBox& SurfaceBounds,
const FTransform& ViewerTransform,
float ViewerFOV,
int32 ScreenWidth,
int32 ScreenHeight)
{
if (!SurfaceBounds.IsValid || ScreenWidth <= 0 || ScreenHeight <= 0)
{
return 0.0f;
}
const float HalfFOVRad = FMath::DegreesToRadians(ViewerFOV * 0.5f);
const FVector ViewerPos = ViewerTransform.GetLocation();
const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X);
// Project all 8 corners of the bounding box to screen space
const FVector Corners[8] = {
FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z),
FVector(SurfaceBounds.Min.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z),
FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z),
FVector(SurfaceBounds.Min.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z),
FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Min.Z),
FVector(SurfaceBounds.Max.X, SurfaceBounds.Min.Y, SurfaceBounds.Max.Z),
FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Min.Z),
FVector(SurfaceBounds.Max.X, SurfaceBounds.Max.Y, SurfaceBounds.Max.Z),
};
float MinScreenX = FLT_MAX, MinScreenY = FLT_MAX;
float MaxScreenX = -FLT_MAX, MaxScreenY = -FLT_MAX;
int32 BehindCameraCount = 0;
for (const FVector& Corner : Corners)
{
const FVector ToCorner = Corner - ViewerPos;
const float DotForward = FVector::DotProduct(ToCorner, ViewerForward);
if (DotForward <= 0.0f)
{
BehindCameraCount++;
continue;
}
const float Distance = ToCorner.Size();
const FVector Right = ViewerTransform.GetUnitAxis(EAxis::Y);
const FVector Up = ViewerTransform.GetUnitAxis(EAxis::Z);
const float DotRight = FVector::DotProduct(ToCorner.GetSafeNormal(), Right);
const float DotUp = FVector::DotProduct(ToCorner.GetSafeNormal(), Up);
const float AngleX = FMath::Atan2(DotRight, DotForward);
const float AngleY = FMath::Atan2(DotUp, DotForward);
const float ScreenX = (AngleX / HalfFOVRad) * (ScreenWidth * 0.5f) + (ScreenWidth * 0.5f);
const float ScreenY = (ScreenHeight * 0.5f) - (AngleY / HalfFOVRad) * (ScreenHeight * 0.5f);
MinScreenX = FMath::Min(MinScreenX, ScreenX);
MinScreenY = FMath::Min(MinScreenY, ScreenY);
MaxScreenX = FMath::Max(MaxScreenX, ScreenX);
MaxScreenY = FMath::Max(MaxScreenY, ScreenY);
}
if (BehindCameraCount >= 8)
{
return 0.0f;
}
// Clamp to screen bounds
MinScreenX = FMath::Clamp(MinScreenX, 0.0f, static_cast<float>(ScreenWidth));
MinScreenY = FMath::Clamp(MinScreenY, 0.0f, static_cast<float>(ScreenHeight));
MaxScreenX = FMath::Clamp(MaxScreenX, 0.0f, static_cast<float>(ScreenWidth));
MaxScreenY = FMath::Clamp(MaxScreenY, 0.0f, static_cast<float>(ScreenHeight));
const float ScreenArea = static_cast<float>(ScreenWidth * ScreenHeight);
const float BoundingArea = (MaxScreenX - MinScreenX) * (MaxScreenY - MinScreenY);
return FMath::Clamp(BoundingArea / ScreenArea, 0.0f, 1.0f);
}
bool UPlanarCaptureCameraUtils::IsSurfaceVisibleToViewer(
const FBox& SurfaceBounds,
const FTransform& ViewerTransform,
float ViewerFOV,
float ViewerAspectRatio,
float ViewerNearPlane,
float ViewerFarPlane)
{
if (!SurfaceBounds.IsValid)
{
return false;
}
// Quick dot-product check: is the surface center roughly in front of the viewer?
const FVector SurfaceCenter = SurfaceBounds.GetCenter();
const FVector ViewerPos = ViewerTransform.GetLocation();
const FVector ToSurface = SurfaceCenter - ViewerPos;
const FVector ViewerForward = ViewerTransform.GetUnitAxis(EAxis::X);
if (FVector::DotProduct(ToSurface.GetSafeNormal(), ViewerForward) < 0.0f)
{
return false;
}
// Distance check
if (ToSurface.Size() > ViewerFarPlane)
{
return false;
}
if (ToSurface.Size() < ViewerNearPlane)
{
return false;
}
// Screen coverage check — if coverage is above 0, it's visible
const float Coverage = ComputeScreenCoverage(SurfaceBounds, ViewerTransform, ViewerFOV, 1920, 1080);
return Coverage > 0.001f;
}
float UPlanarCaptureCameraUtils::ComputeCompositeScore(
float ScreenCoverage,
float FacingAngle,
float DistanceToViewer,
float MaxDistance,
float ScriptedPriority)
{
const float DistanceFactor = (MaxDistance > 0.0f)
? FMath::Clamp(1.0f - (DistanceToViewer / MaxDistance), 0.0f, 1.0f)
: 1.0f;
const float Score = (ScreenCoverage * 0.5f)
+ (FMath::Abs(FacingAngle) * 0.3f)
+ (DistanceFactor * 0.1f)
+ (ScriptedPriority * 0.1f);
return FMath::Clamp(Score, 0.0f, 1.0f);
}

View File

@@ -0,0 +1,6 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — PlanarCaptureCommon implementation (trivial)
#include "Capture/PlanarCaptureCommon.h"
// Struct and enum definitions are header-only.
// This file exists for module compilation unity.

View File

@@ -0,0 +1,434 @@
// 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);
}

View File

@@ -0,0 +1,278 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — BPC_PlanarCapture (136)
// The heart of the Planar Capture System. Attached to any actor that needs
// a scene capture — mirrors, portals, monitors, horror surfaces.
//
// Manages USceneCaptureComponent2D lifetime, render target pool allocation,
// camera transform computation per mode, oblique clip plane injection,
// show/hide actor lists, quality tier application, frame ring buffer,
// and MPC parameter pushes.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Capture/PlanarCaptureCommon.h"
#include "BPC_PlanarCapture.generated.h"
// Forward declarations
class USceneCaptureComponent2D;
class UTextureRenderTarget2D;
class UMaterialInstanceDynamic;
class UMaterialParameterCollection;
class ASS_PlanarCaptureManager;
class ABP_PlanarCaptureActor;
// ============================================================================
// Delegates
// ============================================================================
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnCaptureQualityChanged, EPlanarCaptureQualityTier, OldTier, EPlanarCaptureQualityTier, NewTier);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCaptureInitialized, EPlanarCaptureInitResult, Result);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCaptureRendered);
/**
* BPC_PlanarCapture — Core capture component for mirrors, portals, monitors, etc.
*
* Owns the USceneCaptureComponent2D lifecycle. All camera math, render target
* management, and per-frame capture decisions happen here in C++ for performance.
* Designer configuration flows through Blueprint children.
*
* Multiplayer: Capture is always local-only. Each client renders their own view.
* No replication needed. This component only exists on clients.
*/
UCLASS(Blueprintable, ClassGroup = (Framework), meta = (BlueprintSpawnableComponent))
class PG_FRAMEWORK_API UBPC_PlanarCapture : public UActorComponent
{
GENERATED_BODY()
public:
UBPC_PlanarCapture();
// ========================================================================
// Lifecycle
// ========================================================================
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
// ========================================================================
// Configuration — Set in Blueprint Defaults
// ========================================================================
/** What kind of surface this capture represents. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
EPlanarCaptureMode CaptureMode = EPlanarCaptureMode::Mirror;
/** Quality profiles per tier (0=Low, 1=Medium, 2=High, 3=Hero). Index maps to EPlanarCaptureQualityTier. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
TArray<FPlanarCaptureQualityProfile> QualityProfiles;
/** FOV for the capture camera. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
float CaptureFOV = 90.0f;
/** Maximum view distance for the capture (0 = unlimited). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
float MaxViewDistance = 5000.0f;
// ========================================================================
// Portal Configuration
// ========================================================================
/** For Portal mode: the linked target surface actor. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Portal")
TSoftObjectPtr<ABP_PlanarCaptureActor> LinkedTargetSurface;
// ========================================================================
// Monitor Configuration
// ========================================================================
/** For Monitor mode: a fixed camera actor to capture from. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Monitor")
TSoftObjectPtr<AActor> FixedCameraActor;
// ========================================================================
// Actor Lists
// ========================================================================
/** Actors to show exclusively in the capture (empty = show all). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorLists")
TArray<FPlanarCaptureActorListEntry> ShowOnlyActors;
/** Actors to hide from the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorLists")
TArray<FPlanarCaptureActorListEntry> HiddenActors;
// ========================================================================
// Horror Configuration
// ========================================================================
/** Actor to swap into the ShowOnly list during wrong-reflection events. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Horror")
TSoftObjectPtr<AActor> WrongReflectionActor;
/** Mesh component of the surface plane (for clip plane calculation). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Config")
TSoftObjectPtr<UStaticMeshComponent> SurfaceMeshComponent;
// ========================================================================
// Runtime State (Read-Only)
// ========================================================================
/** Current assigned quality tier (set by SS_PlanarCaptureManager). */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
EPlanarCaptureQualityTier CurrentQualityTier = EPlanarCaptureQualityTier::Off;
/** Is this capture currently active (capturing frames)? */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
bool bIsCapturing = false;
/** The render target this capture writes to. */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
TObjectPtr<UTextureRenderTarget2D> CaptureRenderTarget;
// ========================================================================
// Public API — BlueprintCallable
// ========================================================================
/**
* Initialize the capture component. Allocates render target, creates and
* configures the USceneCaptureComponent2D. Called by the owning actor on BeginPlay.
*/
UFUNCTION(BlueprintCallable, Category = "Capture")
EPlanarCaptureInitResult InitializeCapture();
/**
* Shut down the capture, release render target back to pool, destroy SceneCaptureComponent2D.
*/
UFUNCTION(BlueprintCallable, Category = "Capture")
void ShutdownCapture();
/**
* Apply a quality tier profile immediately. Called by SS_PlanarCaptureManager.
*/
UFUNCTION(BlueprintCallable, Category = "Capture")
void ApplyQualityTier(EPlanarCaptureQualityTier Tier);
/**
* Trigger a single capture frame immediately (bypasses tick interval).
*/
UFUNCTION(BlueprintCallable, Category = "Capture")
void CaptureNow();
/**
* Swap the ShowOnly actor list to the wrong-reflection actor (horror mode).
* Original list is preserved and restored on DeactivateHorrorReflection().
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Horror")
void ActivateHorrorReflection();
/**
* Restore the original ShowOnly actor list after a horror event.
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Horror")
void DeactivateHorrorReflection();
/**
* Push a frame from the ring buffer into the render target for delayed reflection (horror lag).
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Horror")
void PushDelayedFrame();
/**
* Set a scripted priority override (0.0 to 1.0). Higher values force higher quality tier.
* Example: a scare event elevates a specific mirror to Hero tier.
*/
UFUNCTION(BlueprintCallable, Category = "Capture")
void SetScriptedPriority(float Priority);
/**
* Push all Material Parameter Collection values for this surface.
* Called every frame when capturing, or on event trigger for horror params.
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Material")
void PushMPCParameters(UMaterialParameterCollection* MPC);
// ========================================================================
// Compute — BlueprintPure
// ========================================================================
/** Compute the capture camera transform for the current mode. */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture")
FTransform ComputeCaptureCameraTransform(const FTransform& ViewerCameraTransform) const;
/** Get the current composite quality score. */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture")
FPlanarCaptureScore GetCurrentScore() const;
// ========================================================================
// Event Dispatchers
// ========================================================================
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
FOnCaptureQualityChanged OnCaptureQualityChanged;
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
FOnCaptureInitialized OnCaptureInitialized;
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
FOnCaptureRendered OnCaptureRendered;
protected:
// ========================================================================
// Internal State
// ========================================================================
/** The actual UE5 SceneCaptureComponent2D — created at runtime. */
UPROPERTY()
TObjectPtr<USceneCaptureComponent2D> SceneCapture;
/** Cached reference to the manager subsystem. */
UPROPERTY()
TObjectPtr<ASS_PlanarCaptureManager> CachedManager;
/** Cached reference to the owning actor. */
UPROPERTY()
TObjectPtr<ABP_PlanarCaptureActor> CachedOwningActor;
/** Time accumulator for capture interval throttling. */
float TimeSinceLastCapture = 0.0f;
/** Current quality profile (cached from QualityProfiles[Tier]). */
FPlanarCaptureQualityProfile ActiveProfile;
/** Scripted priority override (0.0 to 1.0). */
float ScriptedPriorityOverride = 0.0f;
/** Ring buffer of render targets for delayed reflection (horror mode). */
UPROPERTY()
TArray<TObjectPtr<UTextureRenderTarget2D>> FrameRingBuffer;
/** Current write index into the frame ring buffer. */
int32 RingBufferWriteIndex = 0;
/** Saved ShowOnly list before horror swap. */
TArray<TSoftObjectPtr<AActor>> SavedShowOnlyActors;
// ========================================================================
// Internal Methods
// ========================================================================
/** Create and configure the USceneCaptureComponent2D. */
void CreateSceneCaptureComponent();
/** Apply show flags from the active quality profile to the SceneCapture. */
void ApplyShowFlags();
/** Update the ShowOnly and Hidden actor lists on the SceneCapture. */
void UpdateActorLists();
/** Resolve soft references on initialization. */
void ResolveSoftReferences();
/** Compute the surface plane in world space for clip plane and mirror math. */
FPlane GetSurfacePlane() const;
};

View File

@@ -0,0 +1,170 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — BP_PlanarCaptureActor (137)
// Placeable actor wrapping a BPC_PlanarCapture component. Owns the surface mesh,
// material dynamic instances, and the capture component. Handles overlap/proximity
// events feeding into the quality manager. Registers with the global subsystem.
//
// Blueprint children: BP_Mirror, BP_Portal, BP_Monitor, BP_HorrorMirror, BP_FakeWindow.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Capture/PlanarCaptureCommon.h"
#include "BP_PlanarCaptureActor.generated.h"
class UBPC_PlanarCapture;
class UStaticMeshComponent;
class UMaterialInstanceDynamic;
class UMaterialParameterCollection;
class UBoxComponent;
// Delegates
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlanarCaptureActorModeChanged, EPlanarCaptureMode, NewMode);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlanarCaptureSurfaceDestroyed, ABP_PlanarCaptureActor*, Surface);
/**
* BP_PlanarCaptureActor — Placeable actor for mirrors, portals, monitors, etc.
*
* Blueprint children configure the mode, mesh, materials, and capture settings.
* This actor is the designer-facing interface — all runtime logic lives in
* BPC_PlanarCapture and SS_PlanarCaptureManager.
*
* Multiplayer: Spawned on all clients. Capture rendering is local-only.
* Surface state (destroyed, on/off) may replicate via RepNotify.
*/
UCLASS(Blueprintable, BlueprintType)
class PG_FRAMEWORK_API ABP_PlanarCaptureActor : public AActor
{
GENERATED_BODY()
public:
ABP_PlanarCaptureActor();
// ========================================================================
// Components
// ========================================================================
/** The surface mesh — a plane, quad, or frame. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Capture|Components")
TObjectPtr<UStaticMeshComponent> SurfaceMesh;
/** Proximity trigger volume for quality scoring. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Capture|Components")
TObjectPtr<UBoxComponent> ProximityTrigger;
/** The core capture component. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Capture|Components")
TObjectPtr<UBPC_PlanarCapture> CaptureComponent;
// ========================================================================
// Configuration
// ========================================================================
/** Display name for debug and logging. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
FString SurfaceDisplayName;
/** Whether this surface starts enabled. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
bool bStartEnabled = true;
/** Whether this surface can be destroyed (shattered mirror, broken monitor). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Config")
bool bDestructible = false;
/** Material Parameter Collection for global surface parameters. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capture|Material")
TObjectPtr<UMaterialParameterCollection> SurfaceMPC;
// ========================================================================
// Runtime State
// ========================================================================
/** Whether this surface is currently active. */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
bool bIsActive = false;
/** Dynamic material instance on the surface mesh. */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Runtime")
TObjectPtr<UMaterialInstanceDynamic> SurfaceMaterialInstance;
// ========================================================================
// Public API
// ========================================================================
/** Enable the capture surface. */
UFUNCTION(BlueprintCallable, Category = "Capture")
void EnableSurface();
/** Disable the capture surface (stops rendering, releases budget). */
UFUNCTION(BlueprintCallable, Category = "Capture")
void DisableSurface();
/** Set the capture mode at runtime. */
UFUNCTION(BlueprintCallable, Category = "Capture")
void SetCaptureMode(EPlanarCaptureMode NewMode);
/** Set the surface material at runtime (e.g., swap between clean/dirty mirror). */
UFUNCTION(BlueprintCallable, Category = "Capture")
void SetSurfaceMaterial(UMaterialInterface* NewMaterial);
/** Set a single MPC scalar parameter for this surface. */
UFUNCTION(BlueprintCallable, Category = "Capture|Material")
void SetSurfaceMPCParameter(FName ParameterName, float Value);
/** Destroy the surface (shatter mirror, break monitor). Triggers visual effect. */
UFUNCTION(BlueprintCallable, Category = "Capture")
void DestroySurface();
// ========================================================================
// Overrides
// ========================================================================
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// ========================================================================
// Event Dispatchers
// ========================================================================
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
FOnPlanarCaptureActorModeChanged OnModeChanged;
UPROPERTY(BlueprintAssignable, Category = "Capture|Events")
FOnPlanarCaptureSurfaceDestroyed OnSurfaceDestroyed;
protected:
// ========================================================================
// Overlap Events (Proximity Trigger)
// ========================================================================
UFUNCTION()
void OnProximityBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnProximityEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
// ========================================================================
// RepNotify (multiplayer)
// ========================================================================
UFUNCTION()
void OnRep_IsActive();
/** Replicated active state for server-authoritative surface control. */
UPROPERTY(ReplicatedUsing = OnRep_IsActive)
bool bRepIsActive = false;
// ========================================================================
// Internal
// ========================================================================
/** Register with the global manager subsystem. */
void RegisterWithManager();
/** Create the dynamic material instance from the surface mesh material. */
void CreateMaterialInstance();
};

View File

@@ -0,0 +1,155 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — PlanarCaptureCameraUtils
// Static math library for mirror reflection, portal relative transform,
// oblique projection, screen coverage, and visibility computations.
// All functions are BlueprintCallable and use UE5's FMatrix/FVector/FPlane types.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "PlanarCaptureCameraUtils.generated.h"
/**
* Static math library for planar capture camera transforms.
*
* Mirror: reflect the viewer camera across the mirror plane.
* Portal: compute the relative transform from source surface to target surface.
* All math happens in C++ for performance — Blueprint calls these as pure functions.
*/
UCLASS()
class PG_FRAMEWORK_API UPlanarCaptureCameraUtils : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// ========================================================================
// Mirror Reflection Math
// ========================================================================
/**
* Compute the mirrored camera transform for a planar mirror.
* Reflects the viewer's camera position and rotation across the mirror plane.
*
* @param ViewerCameraTransform World transform of the viewer's camera.
* @param MirrorPlaneTransform World transform of the mirror surface (XY plane, Z = normal).
* @return The world transform for the SceneCapture camera.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
static FTransform ComputeMirroredTransform(
const FTransform& ViewerCameraTransform,
const FTransform& MirrorPlaneTransform);
// ========================================================================
// Portal Relative Math
// ========================================================================
/**
* Compute the capture camera transform for a portal.
* The viewer's position relative to the source surface is mapped to the
* target surface's coordinate space.
*
* @param ViewerCameraTransform World transform of the viewer's camera.
* @param SourceSurfaceTransform World transform of the portal entry surface.
* @param TargetSurfaceTransform World transform of the portal exit surface.
* @return The world transform for the SceneCapture camera.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
static FTransform ComputePortalTransform(
const FTransform& ViewerCameraTransform,
const FTransform& SourceSurfaceTransform,
const FTransform& TargetSurfaceTransform);
// ========================================================================
// Oblique Near-Plane Projection
// ========================================================================
/**
* Compute an oblique near-clip-plane projection matrix.
* Used to prevent geometry behind the portal/mirror surface from clipping
* into the capture view.
*
* @param FOV Horizontal field of view in degrees.
* @param AspectRatio Width / Height.
* @param NearPlane Near clip distance.
* @param FarPlane Far clip distance.
* @param ClipPlane World-space plane to clip against.
* @param SurfaceTransform Transform of the surface (for converting clip plane to view space).
* @return The oblique projection matrix.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
static FMatrix ComputeObliqueProjectionMatrix(
float FOV,
float AspectRatio,
float NearPlane,
float FarPlane,
const FPlane& ClipPlane,
const FTransform& SurfaceTransform);
// ========================================================================
// Screen Coverage & Visibility
// ========================================================================
/**
* Estimate how much of the screen a capture surface occupies (0.0 to 1.0).
* Uses the surface's bounding box corners projected to screen-space.
*
* @param SurfaceBounds Bounding box of the surface mesh in world space.
* @param ViewerTransform World transform of the viewer's camera.
* @param ViewerFOV Horizontal FOV of the viewer.
* @param ScreenWidth Viewport width in pixels.
* @param ScreenHeight Viewport height in pixels.
* @return Estimated screen coverage ratio (0.0 to 1.0).
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
static float ComputeScreenCoverage(
const FBox& SurfaceBounds,
const FTransform& ViewerTransform,
float ViewerFOV,
int32 ScreenWidth,
int32 ScreenHeight);
/**
* Check whether the surface is visible to the viewer's frustum.
*
* @param SurfaceBounds Bounding box of the surface mesh in world space.
* @param ViewerTransform World transform of the viewer's camera.
* @param ViewerFOV Horizontal FOV.
* @param ViewerAspectRatio Width / Height.
* @param ViewerNearPlane Near clip distance.
* @param ViewerFarPlane Far clip distance.
* @return True if any part of the surface is within the frustum.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
static bool IsSurfaceVisibleToViewer(
const FBox& SurfaceBounds,
const FTransform& ViewerTransform,
float ViewerFOV,
float ViewerAspectRatio,
float ViewerNearPlane,
float ViewerFarPlane);
// ========================================================================
// Quality Scoring
// ========================================================================
/**
* Compute a composite priority score for a capture surface.
* Higher score = higher quality tier assignment.
* Formula: (ScreenCoverage * 0.5) + (FacingAngle * 0.3) + (DistanceFactor * 0.1) + (ScriptedPriority * 0.1)
*
* @param ScreenCoverage How much screen real estate the surface occupies.
* @param FacingAngle Dot product of viewer forward to surface normal.
* @param DistanceToViewer Distance in world units.
* @param MaxDistance Distance at which score drops to zero.
* @param ScriptedPriority Priority override from gameplay systems (0.0 to 1.0).
* @return Composite score (0.0 to 1.0).
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|CameraUtils")
static float ComputeCompositeScore(
float ScreenCoverage,
float FacingAngle,
float DistanceToViewer,
float MaxDistance,
float ScriptedPriority);
};

View File

@@ -0,0 +1,189 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — PlanarCaptureCommon
// Shared enums, structs, and quality profile definitions for the Planar Capture System.
// Used by BPC_PlanarCapture, BP_PlanarCaptureActor, and SS_PlanarCaptureManager.
#pragma once
#include "CoreMinimal.h"
#include "Engine/TextureRenderTarget2D.h"
#include "PlanarCaptureCommon.generated.h"
// ============================================================================
// Enums
// ============================================================================
/**
* The mode of the capture surface — determines camera math, rendering behavior,
* and material configuration.
*/
UENUM(BlueprintType)
enum class EPlanarCaptureMode : uint8
{
Mirror UMETA(DisplayName = "Mirror"),
Portal UMETA(DisplayName = "Portal"),
Monitor UMETA(DisplayName = "Monitor / Security Screen"),
HorrorMirror UMETA(DisplayName = "Horror Mirror"),
HorrorPortal UMETA(DisplayName = "Horror Portal"),
FakeWindow UMETA(DisplayName = "Fake Window"),
};
/**
* Quality tier for a capture surface. Managed globally by SS_PlanarCaptureManager.
*/
UENUM(BlueprintType)
enum class EPlanarCaptureQualityTier : uint8
{
Off UMETA(DisplayName = "Off — No capture"),
Low UMETA(DisplayName = "Low — 256px, 4fps"),
Medium UMETA(DisplayName = "Medium — 512px, 15fps"),
High UMETA(DisplayName = "High — 1024px, 30fps"),
Hero UMETA(DisplayName = "Hero — 2048px, 60fps"),
};
/**
* Result codes for capture surface initialization.
*/
UENUM(BlueprintType)
enum class EPlanarCaptureInitResult : uint8
{
Success UMETA(DisplayName = "Success"),
NoRenderTargetPool UMETA(DisplayName = "Failed — No Render Target in Pool"),
InvalidSurfaceMesh UMETA(DisplayName = "Failed — Invalid Surface Mesh"),
BudgetExceeded UMETA(DisplayName = "Failed — Budget Exceeded"),
ManagerUnavailable UMETA(DisplayName = "Failed — Manager Unavailable"),
};
// ============================================================================
// Structs
// ============================================================================
/**
* Quality profile — defines render target resolution, capture interval,
* and per-feature toggles for a single quality tier.
*/
USTRUCT(BlueprintType)
struct PG_FRAMEWORK_API FPlanarCaptureQualityProfile
{
GENERATED_BODY()
/** Render target resolution (square: 256, 512, 1024, 2048). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Quality")
int32 RenderTargetSize = 512;
/** Minimum interval between captures in seconds (1.0 / FPS). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Quality")
float CaptureInterval = 0.0667f; // ~15fps
/** Shadow rendering mode for the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableShadows = true;
/** Allow post-process effects in the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnablePostProcess = false;
/** Allow exponential height fog in the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableFog = false;
/** Allow bloom in the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableBloom = false;
/** Allow ambient occlusion in the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableAO = false;
/** Allow Lumen global illumination in the capture (expensive). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableLumen = false;
/** Allow motion blur in the capture. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableMotionBlur = false;
/** Enable oblique near-clip plane (required for portals flush with geometry). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Features")
bool bEnableClipPlane = true;
/** Number of frames to delay the reflection (0 = off, N = horror lag effect). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Horror", meta = (ClampMin = "0", ClampMax = "30"))
int32 DelayedFrameCount = 0;
};
/**
* Single entry in a capture surface's ShowOnly or Hidden actor list.
*/
USTRUCT(BlueprintType)
struct PG_FRAMEWORK_API FPlanarCaptureActorListEntry
{
GENERATED_BODY()
/** The actor to show exclusively or hide. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorList")
TSoftObjectPtr<AActor> Actor;
/** Whether this actor is currently active in the list. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|ActorList")
bool bActive = true;
};
/**
* Scoring data for a capture surface — computed each frame by the manager
* to determine quality tier assignment.
*/
USTRUCT(BlueprintType)
struct PG_FRAMEWORK_API FPlanarCaptureScore
{
GENERATED_BODY()
/** Distance from viewer to surface in world units. */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
float DistanceToViewer = FLT_MAX;
/** Screen coverage percentage (0.0 to 1.0). */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
float ScreenCoverage = 0.0f;
/** Dot product of viewer forward and surface normal (1.0 = facing, 0.0 = edge-on). */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
float FacingAngle = 0.0f;
/** Whether the surface is within the viewer's frustum. */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
bool bInFrustum = false;
/** Scripted priority override (0.0 to 1.0, from gameplay systems). */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
float ScriptedPriority = 0.0f;
/** Composite score (0.0 to 1.0) — higher = more important. */
UPROPERTY(BlueprintReadOnly, Category = "Capture|Score")
float CompositeScore = 0.0f;
};
/**
* Render target pool entry — managed by SS_PlanarCaptureManager.
*/
USTRUCT()
struct PG_FRAMEWORK_API FPlanarCaptureRenderTargetEntry
{
GENERATED_BODY()
/** The allocated render target. */
UPROPERTY()
TObjectPtr<UTextureRenderTarget2D> RenderTarget = nullptr;
/** Current size of the render target. */
UPROPERTY()
int32 CurrentSize = 0;
/** Whether this entry is currently assigned to a surface. */
UPROPERTY()
bool bInUse = false;
/** Which surface currently owns this entry. */
UPROPERTY()
TWeakObjectPtr<class ABP_PlanarCaptureActor> OwningSurface;
};

View File

@@ -0,0 +1,232 @@
// Copyright Ngonart OU. All Rights Reserved.
// UE5 Modular Game Framework — SS_PlanarCaptureManager (138)
// Global budget manager for all planar capture surfaces in the world.
//
// One instance per World (World Subsystem). Each frame, scores every registered
// capture surface by distance, screen coverage, facing angle, and scripted priority.
// Assigns quality tiers across all surfaces respecting a global budget
// (max simultaneous high-quality captures, max total render target memory).
// Forces idle/disabled state on surfaces outside active rooms/sublevels.
//
// Also manages the render target pool — allocates, reuses, and resizes RTs
// to minimize memory churn.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "Capture/PlanarCaptureCommon.h"
#include "SS_PlanarCaptureManager.generated.h"
class ABP_PlanarCaptureActor;
class UBPC_PlanarCapture;
// Delegates
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSurfaceRegistered, ABP_PlanarCaptureActor*, Surface, int32, TotalSurfaces);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSurfaceUnregistered, ABP_PlanarCaptureActor*, Surface, int32, TotalSurfaces);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGlobalQualityCapChanged, EPlanarCaptureQualityTier, NewCap);
/**
* SS_PlanarCaptureManager — Global Planar Capture Budget Manager.
*
* Manages all ABP_PlanarCaptureActor instances per world. Evaluates priority
* and assigns quality tiers to stay within a configurable budget.
* Also owns the render target pool to reduce memory allocation overhead.
*
* Multiplayer: This subsystem exists on both server and clients.
* On the server, it tracks surfaces for replication state.
* On clients, it drives actual capture rendering.
*/
UCLASS()
class PG_FRAMEWORK_API ASS_PlanarCaptureManager : public UWorldSubsystem
{
GENERATED_BODY()
public:
ASS_PlanarCaptureManager();
// ========================================================================
// Lifecycle
// ========================================================================
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override;
// ========================================================================
// Surface Registry
// ========================================================================
/**
* Register a capture surface actor with the global manager.
* Called by ABP_PlanarCaptureActor::BeginPlay.
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
void RegisterSurface(ABP_PlanarCaptureActor* Surface);
/**
* Unregister a capture surface actor. Called on EndPlay.
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
void UnregisterSurface(ABP_PlanarCaptureActor* Surface);
/**
* Get all currently registered surfaces.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
TArray<ABP_PlanarCaptureActor*> GetRegisteredSurfaces() const;
/**
* Get the number of registered surfaces.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
int32 GetSurfaceCount() const { return RegisteredSurfaces.Num(); }
// ========================================================================
// Quality Budget Management
// ========================================================================
/**
* Global quality ceiling — caps all surfaces at this tier regardless of score.
* Use this for lower-end hardware targets.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
EPlanarCaptureQualityTier GlobalQualityCap = EPlanarCaptureQualityTier::High;
/**
* Maximum number of simultaneous Hero-tier captures.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
int32 MaxHeroSurfaces = 1;
/**
* Maximum number of simultaneous High-tier captures.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
int32 MaxHighSurfaces = 3;
/**
* Maximum number of simultaneous Medium-tier captures.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
int32 MaxMediumSurfaces = 6;
/**
* Maximum total render target memory in megabytes across all surfaces.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
float MaxTotalRenderTargetMemoryMB = 128.0f;
/**
* Distance at which capture quality drops to Off (world units).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
float MaxCaptureDistance = 10000.0f;
/**
* Interval between full re-evaluation of all surfaces (seconds).
* Individual surfaces check their own interval every frame via their component.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Capture|Manager|Budget")
float FullEvaluationInterval = 0.5f;
// ========================================================================
// Render Target Pool
// ========================================================================
/**
* Request a render target from the pool. Returns nullptr if none available.
*/
UTextureRenderTarget2D* RequestRenderTarget(int32 Size);
/**
* Release a render target back to the pool.
*/
void ReleaseRenderTarget(UTextureRenderTarget2D* RenderTarget);
/**
* Get the total memory used by the render target pool (in MB).
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
float GetPoolMemoryUsageMB() const;
// ========================================================================
// Query
// ========================================================================
/**
* Get the nearest capture surface of a given mode to a world location.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Capture|Manager")
ABP_PlanarCaptureActor* GetNearestSurfaceOfMode(
EPlanarCaptureMode Mode, FVector WorldLocation, float MaxDistance = 0.0f) const;
/**
* Force all surfaces to a specific quality tier (e.g., for cutscenes).
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
void ForceAllSurfacesToTier(EPlanarCaptureQualityTier Tier);
/**
* Release the force-tier override and resume normal scoring.
*/
UFUNCTION(BlueprintCallable, Category = "Capture|Manager")
void ReleaseForceTier();
// ========================================================================
// Event Dispatchers
// ========================================================================
UPROPERTY(BlueprintAssignable, Category = "Capture|Manager|Events")
FOnSurfaceRegistered OnSurfaceRegistered;
UPROPERTY(BlueprintAssignable, Category = "Capture|Manager|Events")
FOnSurfaceUnregistered OnSurfaceUnregistered;
UPROPERTY(BlueprintAssignable, Category = "Capture|Manager|Events")
FOnGlobalQualityCapChanged OnGlobalQualityCapChanged;
protected:
// ========================================================================
// Internal State
// ========================================================================
/** All registered capture surface actors. */
UPROPERTY()
TArray<TWeakObjectPtr<ABP_PlanarCaptureActor>> RegisteredSurfaces;
/** Render target pool. */
TArray<FPlanarCaptureRenderTargetEntry> RenderTargetPool;
/** Time accumulator for full re-evaluation interval. */
float TimeSinceLastEvaluation = 0.0f;
/** Force-tier override — if set, all surfaces use this tier. */
TOptional<EPlanarCaptureQualityTier> ForceTierOverride;
/** Count of surfaces at each tier (tracked for budget enforcement). */
TMap<EPlanarCaptureQualityTier, int32> TierAssignmentCounts;
// ========================================================================
// Internal Methods
// ========================================================================
/** Evaluate all registered surfaces and assign quality tiers. */
void EvaluateAllSurfaces();
/**
* Score a single surface and determine its quality tier within budget constraints.
* @return The assigned tier.
*/
EPlanarCaptureQualityTier ScoreAndAssignTier(UBPC_PlanarCapture* Capture);
/** Enforce budget limits — demote lower-priority surfaces if budget exceeded. */
void EnforceBudgetLimits();
/** Create a new render target of the given size. */
UTextureRenderTarget2D* CreateRenderTarget(int32 Size);
/** Get or create a render target for a given size (first checks pool). */
UTextureRenderTarget2D* GetOrCreateRenderTarget(int32 Size);
};