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:
581
Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp
Normal file
581
Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp
Normal 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);
|
||||
}
|
||||
241
Source/PG_Framework/Private/Capture/BP_PlanarCaptureActor.cpp
Normal file
241
Source/PG_Framework/Private/Capture/BP_PlanarCaptureActor.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
240
Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp
Normal file
240
Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp
Normal 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);
|
||||
}
|
||||
@@ -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.
|
||||
434
Source/PG_Framework/Private/Capture/SS_PlanarCaptureManager.cpp
Normal file
434
Source/PG_Framework/Private/Capture/SS_PlanarCaptureManager.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user