Files
UE5-Modular-Game-Framework/Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp

582 lines
17 KiB
C++

// 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<USS_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);
}