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