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

@@ -68,6 +68,22 @@ Content/
S_ActionPermissionResult.uasset # Permission result struct
DA_StateGatingTable.uasset # All gating rules (37 action rules)
BPC_StateManager.uasset # Central state authority component
Capture/ # Planar Capture assets (NEW — Phase 17)
BP_Mirror.uasset # Standard mirror surface
BP_Portal.uasset # Portal surface with linked target
BP_Monitor.uasset # Security camera monitor
BP_HorrorMirror.uasset # Horror mirror with wrong reflection
BP_FakeWindow.uasset # Architectural fake window
M_CaptureSurface_Master.uasset # Master unlit material (9-layer)
MPC_CaptureSurface.uasset # Global MPC (10 scalar params)
DA_PlanarCaptureProfile.uasset # Per-surface capture config
MI_Mirror_Clean.uasset # Material instances
MI_Mirror_Dirty.uasset
MI_Mirror_Steam.uasset
MI_Mirror_Horror.uasset
MI_Portal_Standard.uasset
MI_Monitor_Security.uasset
MI_FakeWindow_Interior.uasset
docs/
blueprints/
@@ -90,6 +106,7 @@ docs/
14-data-assets/ # Data Asset definitions (16 files, 115-127, 129, 134-135)
15-input/ # Enhanced Input System (1 file, 128)
16-state/ # State Management (2 files, 130-131)
17-capture/ # Planar Capture System (12 files, 136-147)
developer/ # Developer Reference Docs (NEW)
INDEX.md # Master developer docs index (12 docs)
architecture-overview.md # Framework-wide architecture walkthrough
@@ -106,7 +123,7 @@ docs/
09-ai-systems.md # AI, perception & encounters explained
10-adaptive-systems.md # Adaptive environment & atmosphere explained
11-16-systems.md # Meta, Settings, Polish, Data Assets, Input, State explained
architecture/ # Architecture & Design Documents (8 files)
architecture/ # Architecture & Design Documents (9 files)
bpc-statemanager.md # BPC_StateManager full spec + Chooser Table Audit
metasounds-audio-system.md # MetaSounds audio architecture (mix buses, room zones, settings)
animation-catalog.md # Animation requirements for all 135 systems
@@ -115,6 +132,7 @@ docs/
hud-overview.md # HUD system architecture — 14 widgets, wiring, integration points
multiplayer-networking.md # Multiplayer networking architecture — authority model, RPCs, prediction
blueprint-limitations-workarounds.md # UE5 C++-only functions + 100% Blueprint workarounds (NEW)
planar-capture-system.md # Planar Capture System — mirrors, portals, monitors, horror surfaces (Phase 17)
CLEAN_SLATE_PLAN.md # Clean slate refactoring plan
reports/ # Condensed audit reports
blueprint_condensed_summary.md # ASK agent audit of all 129 files
@@ -123,7 +141,7 @@ docs/
enhanced-input-system.md
bpc-statemanager.md # NEW — State Manager implementation checklist
```
**Total: 135 numbered Blueprint files + 5 enums + 5 Data Assets + 11 Data Table CSVs + TEMPLATE.md + AUDIT_REPORT.md + INDEX.md + 12 developer docs + 8 architecture docs + 1 audit report = 178 files in 19 directory groups**
**Total: 147 numbered Blueprint files + 5 enums + 5 Data Assets + 11 Data Table CSVs + TEMPLATE.md + AUDIT_REPORT.md + INDEX.md + 13 developer docs + 9 architecture docs + 1 audit report = 192 files in 20 directory groups**
## Naming Conventions
| Prefix | Type |
@@ -180,6 +198,7 @@ docs/
- **Phase 14:** State Management (BPC_StateManager + 4 enums + 3 structs + DA_StateGatingTable — 130, 131)
- **Phase 15:** MetaSounds Audio (SS_AudioManager + BP_RoomAudioZone + DA_AudioSettings + DA_RoomAcousticPreset — 132-135)
- **Phase 16:** Multiplayer Networking — All systems updated with `HasAuthority()` gates, `Server_` RPCs, `RepNotify` handlers, and client prediction patterns. See [`docs/architecture/multiplayer-networking.md`](docs/architecture/multiplayer-networking.md).
- **Phase 17:** Planar Capture System — Mirrors, portals, monitors, horror surfaces. C++ core (136-138) + Blueprint actors (139-143) + Material/MPC (144-145) + Data Assets (146-147). See [`docs/architecture/planar-capture-system.md`](docs/architecture/planar-capture-system.md).
## Key Communication Rules
1. Gameplay Tags + Subsystem lookup (global, decoupled)
@@ -235,4 +254,4 @@ No PR is accepted without these files being current. This ensures the animator,
- **Phase 8-13:** COMPLETE AUDIT_REPORT updated, CONTEXT.md updated, verification, final commit
- **Phase 14-15:** COMPLETE State Management (130-131) + MetaSounds Audio (132-135) blueprint specs created
- **Phase 16:** IN PROGRESS Multiplayer Networking: all 135 blueprint specs being updated with replication stubs, RPC definitions, and server-authoritative patterns. Developer docs updated. Architecture doc created at [`docs/architecture/multiplayer-networking.md`](docs/architecture/multiplayer-networking.md)
- **Remaining:** Cross-reference pass across all 135 files; deprecated BPC_AudioAtmosphereController (95) references to update
- **Remaining:** Cross-reference pass across all 147 files; deprecated BPC_AudioAtmosphereController (95) references to update; Planar Capture System (Phase 17) C++ written, blueprint specs created, pending UE5 editor compile and BP child creation

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

View File

@@ -0,0 +1,202 @@
# Planar Capture System Architecture — UE5 Modular Game Framework
**Version:** 1.0 | **Systems:** 136-147 (12 systems) | **Phase:** 17 — Rendering & Visual
This document specifies the architecture of the Planar Capture System — a unified rendering pipeline for mirrors, portals, monitors, and horror surfaces. One capture pipeline, one quality manager, one material interface.
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ BLUEPRINT LAYER (Designer-Facing) │
│ BP_Mirror BP_HorrorMirror BP_Portal BP_Monitor BP_FakeWindow │
│ DA_PlanarCaptureProfile MPC_CaptureSurface MI_Mirror_Clean/Dirty/Steam │
├─────────────────────────────────────────────────────────────────────────────┤
│ C++ LAYER (Performance-Critical) │
│ UBPC_PlanarCapture — SceneCapture2D lifecycle, camera math │
│ ABP_PlanarCaptureActor — Surface mesh, MDI, overlap/proximity │
│ ASS_PlanarCaptureManager — Global budget, RT pool, scoring │
│ UPlanarCaptureCameraUtils — Mirror/Portal/Oblique math (static) │
├─────────────────────────────────────────────────────────────────────────────┤
│ UE5 ENGINE │
│ USceneCaptureComponent2D UTextureRenderTarget2D UMaterialParameterColl │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## System Map (12 Systems — Files 136-147)
| # | System | Type | Parent Class | Purpose |
|---|--------|------|-------------|---------|
| 136 | `BPC_PlanarCapture` | BPC_ Component | `ActorComponent` | Core capture: SceneCapture2D lifecycle, camera math, RT management, actor lists, horror ring buffer |
| 137 | `BP_PlanarCaptureActor` | BP_ Actor | `Actor` | Placeable surface actor: mesh, MDI, proximity trigger, manager registration |
| 138 | `SS_PlanarCaptureManager` | SS_ Subsystem | `WorldSubsystem` | Global budget manager: surface scoring, quality tier assignment, RT pool |
| 139 | `BP_Mirror` | BP_ Actor | `BP_PlanarCaptureActor` | Standard mirror: Mode=Mirror, dirt/steam/condensation layers, aging |
| 140 | `BP_Portal` | BP_ Actor | `BP_PlanarCaptureActor` | Portal surface: Mode=Portal, linked target, teleport on overlap, clip plane |
| 141 | `BP_Monitor` | BP_ Actor | `BP_PlanarCaptureActor` | Security screen/TV: Mode=Monitor, fixed camera ref, low update rate, scanlines |
| 142 | `BP_HorrorMirror` | BP_ Actor | `BP_Mirror` | Horror mirror: wrong reflection actor, delayed frame, steam text reveal, scare events |
| 143 | `BP_FakeWindow` | BP_ Actor | `BP_PlanarCaptureActor` | Architectural fake window: Mode=FakeWindow, linked sublevel, weather overlay |
| 144 | `M_CaptureSurface_Master` | Material | `Material` | Master unlit material: RT sample, condensation, dirt, steam, horror layers |
| 145 | `MPC_CaptureSurface` | MPC | `MaterialParameterCollection` | Global MPC: SteamIntensity, DirtOpacity, MirrorDarkness, WrongReflectionBlend, etc. (10 params) |
| 146 | `DA_PlanarCaptureProfile` | DA_ Data Asset | `PrimaryDataAsset` | Per-surface capture config: default mode, quality profile overrides, actor lists |
| 147 | `MI_Mirror_Clean/Dirty/Steam/Horror` | MI_ Instances | `M_CaptureSurface_Master` | Pre-configured material instances for common mirror states |
---
## Enums
### `EPlanarCaptureMode` (C++: defined in `PlanarCaptureCommon.h`)
| Value | Description |
|-------|-------------|
| `Mirror` | Standard planar mirror reflection |
| `Portal` | Portal with linked target surface |
| `Monitor` | Fixed-camera security screen / TV |
| `HorrorMirror` | Mirror with horror features (wrong reflection, delayed frame) |
| `HorrorPortal` | Portal with horror features |
| `FakeWindow` | Architectural fake window (parallax, weather) |
### `EPlanarCaptureQualityTier` (C++: defined in `PlanarCaptureCommon.h`)
| Value | RT Size | FPS | Shadows | Lumen | Post | Clip Plane |
|-------|---------|-----|---------|-------|------|-----------|
| `Off` | — | 0 | — | — | — | — |
| `Low` | 256 | 4 | No | No | No | No |
| `Medium` | 512 | 15 | Dynamic | Optional | No | Yes |
| `High` | 1024 | 30 | Full | Yes | Minimal | Yes |
| `Hero` | 2048 | 60 | Full | Yes | Full | Yes |
---
## Communication Matrix
| Source | Target | Method | What |
|--------|--------|--------|------|
| `BP_PlanarCaptureActor` | `SS_PlanarCaptureManager` | Direct (RegisterSurface) | Registers on BeginPlay |
| `SS_PlanarCaptureManager` | `BPC_PlanarCapture` | Direct (ApplyQualityTier) | Assigns quality tier |
| `BPC_PlanarCapture` | `USceneCaptureComponent2D` | Direct (owns) | Camera math + CaptureScene |
| `BPC_PlanarCapture` | `MPC_CaptureSurface` | Direct (SetScalarParameter) | Pushes steam/dirt/horror params |
| `BP_HorrorMirror` | `BPC_ScareEventSystem` (101) | Interface / Dispatcher | Triggers coordinated scares |
| `BP_HorrorMirror` | `SS_AudioManager` (132) | Direct | Plays mirror horror SFX |
| `BPC_PlanarCapture` | `BPC_StateManager` (130) | IsActionPermitted() | Checks if capture can activate |
| `BP_Portal` | Player Pawn | Overlap Event | Teleports player through portal |
---
## Quality Scoring Algorithm
```
CompositeScore = (ScreenCoverage × 0.5) + (FacingAngle × 0.3) + (DistanceFactor × 0.1) + (ScriptedPriority × 0.1)
Tier assignment:
Score ≥ 0.8 → Hero (capped at GlobalQualityCap, max MaxHeroSurfaces)
Score ≥ 0.5 → High (capped at max MaxHighSurfaces)
Score ≥ 0.2 → Medium (capped at max MaxMediumSurfaces)
Score > 0 → Low
Score = 0 or !inFrustum → Off
```
---
## Material Layer Stack (M_CaptureSurface_Master)
```
Layer 0: Render target sample (UV-flipped for mirror, straight for portal/monitor)
Layer 1: Condensation normal map → distorts UV of RT sample
Layer 2: Fresnel edge fade mask
Layer 3: Dirt/scratch multiply (DirtOpacity MPC param)
Layer 4: Steam/fog lerp (animated noise, SteamIntensity MPC param)
Layer 5: Steam emissive glow (backlit fog, SteamEmissiveIntensity)
Layer 6: Text reveal mask lerp (TextRevealProgress, uses steam as carrier)
Layer 7: Mirror darkness multiply (MirrorDarkness MPC param)
Layer 8: Wrong reflection crossfade (WrongReflectionBlend MPC param)
Output: Unlit shading model
```
---
## Horror Features
| Feature | Driven By | Layer |
|---------|-----------|-------|
| Mirror-only ghost actor | ShowOnly list in C++ | C++ |
| Wrong reflection Metahuman swap | ActivateHorrorReflection() | C++ |
| Delayed frame reflection | FrameRingBuffer + DelayedReflectionBlend MPC | C++ + Material |
| Steam fogging | SteamIntensity MPC → Layer 4 | Material |
| Dirt / scratches | DirtOpacity MPC → Layer 3 | Material |
| Condensation rivulets | CondensationFlow MPC → Layer 1 | Material |
| UV distortion / breathing | DistortionAmplitude MPC → Layer 1 | Material |
| Text in steam reveal | TextRevealProgress MPC → Layer 6 | Material |
| Mirror goes dark | MirrorDarkness MPC → Layer 7 | Material |
| Wrong reflection crossfade | WrongReflectionBlend MPC → Layer 8 | Material |
| Surface aging/oxidation | SurfaceAge MPC → Layer 3 (tint) | Material |
---
## Blueprint Limitations & Workarounds
### `USceneCaptureComponent2D::CaptureScene()` (C++ Only)
**Workaround:** Call from C++ in `UBPC_PlanarCapture`. Blueprint calls `CaptureNow()` which invokes C++ capture.
**Affected Files:** 136, 139-143
### `FSceneView::ViewMatrices` for Oblique Projection (C++ Only)
**Workaround:** Use `UPlanarCaptureCameraUtils::ComputeObliqueProjectionMatrix()` (BlueprintCallable).
**Affected Files:** 136, 140
### `USceneCaptureComponent2D::ShowOnlyActors` Runtime Modification (Partial C++)
**Workaround:** `UBPC_PlanarCapture::UpdateActorLists()` resolves TSoftObjectPtrs into the array.
**Affected Files:** 136, 142
---
## Performance Budget Guidelines
| Tier | Max Simultaneous | RT Memory per Surface | GPU Cost |
|------|-----------------|----------------------|----------|
| Hero | 1 | 16 MB (2048² × 4B) | Very High |
| High | 3 | 4 MB (1024² × 4B) | High |
| Medium | 6 | 1 MB (512² × 4B) | Medium |
| Low | Unlimited | 256 KB (256² × 4B) | Low |
| Off | — | 0 | None |
**Total Budget:** 128 MB default (configurable via `SS_PlanarCaptureManager.MaxTotalRenderTargetMemoryMB`)
---
## Multiplayer Considerations
- Capture rendering is **local-only** — each client renders their own view
- `BP_PlanarCaptureActor` replicates `bIsActive` for server-authoritative surface control
- Horror events (wrong reflection activation) are triggered server-side and replicated via gameplay tags
- No client prediction needed — captures are cosmetic
---
## Integration Points with Existing Systems
| Existing System | Integration |
|----------------|-------------|
| `BPC_StateManager` (130) | Query `IsActionPermitted()` before enabling capture, teleport gating |
| `BPC_ScareEventSystem` (101) | Horror mirror events trigger coordinated scares |
| `SS_AudioManager` (132) | Mirror/portal SFX route through audio subsystem |
| `SS_EnhancedInputManager` (128) | Input context switch for portal inspection mode |
| `BPC_DiegeticDisplay` (18) | Monitors can display diegetic UI render targets |
| `BPC_CameraStateLayer` (14) | Camera FOV adjustments during portal transitions |
| `I_Persistable` (36) | Mirror surface state (destroyed, dirty, oxidized) saves/loads |
| `DA_ScareEvent` (127) | Mirror apparition scare event data asset |
| `WBP_SettingsMenu` (57) | Capture surface quality settings (Performance section) |
---
## Build Order (Phase 17)
1. **Phase 17a:** C++ Core — `PlanarCaptureCommon.h`, `PlanarCaptureCameraUtils`, `BPC_PlanarCapture`, `SS_PlanarCaptureManager`, `BP_PlanarCaptureActor`
2. **Phase 17b:** Material Foundation — `M_CaptureSurface_Master`, `MPC_CaptureSurface`, `MI_Mirror_*` instances
3. **Phase 17c:** Blueprint Actors — `BP_Mirror`, `BP_Portal`, `BP_Monitor`, `BP_HorrorMirror`, `BP_FakeWindow`
4. **Phase 17d:** Data Assets — `DA_PlanarCaptureProfile`
5. **Phase 17e:** Integration — Wire horror events to `BPC_ScareEventSystem`, audio to `SS_AudioManager`, persistence to `I_Persistable`
---
*Planar Capture System Architecture v1.0 — See `docs/blueprints/17-capture/` for per-system blueprint specs.*

View File

@@ -0,0 +1,157 @@
# 136 — Planar Capture Component (`BPC_PlanarCapture`)
## Purpose
Core capture component managing `USceneCaptureComponent2D` lifecycle for mirrors, portals, monitors, and horror surfaces. All camera math, render target management, and per-frame capture decisions happen here in C++ for performance.
## Dependencies
- **Requires:** `SS_PlanarCaptureManager` (138) for quality tier assignment and RT pool, `UPlanarCaptureCameraUtils` for camera math
- **Required By:** `BP_PlanarCaptureActor` (137) — owned by parent actor
- **Engine/Plugin Requirements:** Renderer, RenderCore modules
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `ActorComponent` (C++ `UBPC_PlanarCapture`) |
| **Class Type** | Blueprint Component (C++ Full Implementation) |
| **Asset Path** | `Content/Framework/Capture/` |
| **C++ Header** | `Source/PG_Framework/Public/Capture/BPC_PlanarCapture.h` |
| **C++ Source** | `Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp` |
| **C++ Status** | ✅ Full Implementation |
| **BP Asset** | None — use C++ component directly on actors |
## 1. Enums
*Defined in `PlanarCaptureCommon.h` — see Architecture doc for full listing.*
| Enum | Values | Description |
|------|--------|-------------|
| `EPlanarCaptureMode` | Mirror, Portal, Monitor, HorrorMirror, HorrorPortal, FakeWindow | Capture surface mode |
| `EPlanarCaptureQualityTier` | Off, Low, Medium, High, Hero | Quality tier levels |
| `EPlanarCaptureInitResult` | Success, NoRenderTargetPool, InvalidSurfaceMesh, BudgetExceeded, ManagerUnavailable | Init result codes |
## 2. Structs
*Defined in `PlanarCaptureCommon.h`*
### `FPlanarCaptureQualityProfile`
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `RenderTargetSize` | int32 | 512 | Square RT resolution (256/512/1024/2048) |
| `CaptureInterval` | float | 0.0667 | Min seconds between captures (~15fps default) |
| `bEnableShadows` | bool | true | Shadow rendering toggle |
| `bEnablePostProcess` | bool | false | Post-process toggle |
| `bEnableFog` | bool | false | Exponential height fog toggle |
| `bEnableBloom` | bool | false | Bloom toggle |
| `bEnableAO` | bool | false | Ambient occlusion toggle |
| `bEnableLumen` | bool | false | Lumen GI toggle (expensive) |
| `bEnableMotionBlur` | bool | false | Motion blur toggle |
| `bEnableClipPlane` | bool | true | Oblique near-plane toggle |
| `DelayedFrameCount` | int32 | 0 | Horror delayed frame ring buffer size |
## 3. Variables
### Configuration (EditAnywhere, BlueprintReadOnly)
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `CaptureMode` | EPlanarCaptureMode | Mirror | `Capture\|Config` | Surface type |
| `QualityProfiles` | TArray〈FPlanarCaptureQualityProfile〉 | 4 entries | `Capture\|Config` | Quality profiles per tier [0=Low,1=Med,2=High,3=Hero] |
| `CaptureFOV` | float | 90.0 | `Capture\|Config` | Capture camera FOV |
| `MaxViewDistance` | float | 5000.0 | `Capture\|Config` | Max render distance |
| `ShowOnlyActors` | TArray〈FPlanarCaptureActorListEntry〉 | — | `Capture\|ActorLists` | Exclusive show actors |
| `HiddenActors` | TArray〈FPlanarCaptureActorListEntry〉 | — | `Capture\|ActorLists` | Hidden actors |
| `WrongReflectionActor` | TSoftObjectPtr〈AActor〉 | — | `Capture\|Horror` | Horror mode wrong reflection swap |
| `SurfaceMeshComponent` | TSoftObjectPtr〈UStaticMeshComponent〉 | — | `Capture\|Config` | Mesh for clip plane calculation |
| `LinkedTargetSurface` | TSoftObjectPtr〈ABP_PlanarCaptureActor〉 | — | `Capture\|Portal` | Portal destination surface |
| `FixedCameraActor` | TSoftObjectPtr〈AActor〉 | — | `Capture\|Monitor` | Monitor fixed camera |
### Runtime (BlueprintReadOnly)
| Variable | Type | Description |
|----------|------|-------------|
| `CurrentQualityTier` | EPlanarCaptureQualityTier | Assigned quality tier |
| `bIsCapturing` | bool | Is capture currently active |
| `CaptureRenderTarget` | UTextureRenderTarget2D* | Active render target |
## 4. Functions
### Public (BlueprintCallable)
#### `InitializeCapture()` → `EPlanarCaptureInitResult`
- **Description:** Allocates RT from pool, creates USceneCaptureComponent2D, configures show flags
- **Flow:** Request RT → Create SceneCapture → ApplyShowFlags → UpdateActorLists → Set bIsCapturing
- **Nodes Used:** `RequestRenderTarget`, `CreateSceneCaptureComponent2D`, `ApplyShowFlags`, `UpdateActorLists`
#### `ShutdownCapture()`
- **Description:** Stops capture, releases RT to pool, destroys SceneCapture2D
- **Flow:** Set bIsCapturing=false → DestroyComponent → ReleaseRenderTarget → Clear ring buffer
#### `ApplyQualityTier(Tier: EPlanarCaptureQualityTier)`
- **Description:** Apply a quality tier profile immediately. Called by SS_PlanarCaptureManager.
- **Flow:** If Off → ShutdownCapture. If transitioning from Off → InitializeCapture. Else → UpdateShowFlags + ApplyProfile.
#### `CaptureNow()`
- **Description:** Bypasses tick interval, captures immediately. Used for event-driven captures.
- **Blueprint Authority:** Any (local)
- **Flow:** Get viewer camera → ComputeCaptureCameraTransform → Set SceneCapture transform → CaptureScene → PushMPCParameters
#### `ActivateHorrorReflection()`
- **Description:** Saves original ShowOnly list, swaps to WrongReflectionActor
- **Flow:** Save ShowOnlyActors → Clear list → Add WrongReflectionActor → UpdateActorLists
#### `DeactivateHorrorReflection()`
- **Description:** Restores original ShowOnly list after horror event
- **Flow:** Clear list → Restore from SavedShowOnlyActors → UpdateActorLists
#### `SetScriptedPriority(Priority: float)`
- **Description:** Scripted priority override (0.0-1.0). Higher values force higher quality tier.
#### `PushMPCParameters(MPC: UMaterialParameterCollection*)`
- **Description:** Push all 10 MPC scalar parameters for surface material effects
### Compute (BlueprintPure)
#### `ComputeCaptureCameraTransform(ViewerCamera: FTransform)` → `FTransform`
- **Description:** Computes capture camera position based on mode (mirror reflection, portal relative, etc.)
#### `GetCurrentScore()` → `FPlanarCaptureScore`
- **Description:** Returns current composite quality score for this surface
## 5. Event Dispatchers
| Dispatcher | Parameters | Description |
|------------|-----------|-------------|
| `OnCaptureQualityChanged` | OldTier, NewTier | Quality tier changed |
| `OnCaptureInitialized` | Result: EPlanarCaptureInitResult | Init completed |
| `OnCaptureRendered` | — | Each frame rendered |
## 6. Communication Matrix
| Target System | Method | What |
|---------------|--------|------|
| `SS_PlanarCaptureManager` | Direct (cached reference) | RT requests, quality tier reception |
| `BP_PlanarCaptureActor` | Direct (owner) | Surface mesh, MPC reference |
| `UPlanarCaptureCameraUtils` | Static function calls | Mirror/portal/oblique math |
| `USceneCaptureComponent2D` | Direct (owns) | Full lifecycle control |
## 7. Manual Implementation Guide
*The C++ component is fully functional. Blueprint users interact through the public API:*
1. To force a capture (e.g., on a sequencer event):
```
Get BPC_PlanarCapture → Call CaptureNow()
```
2. To trigger a horror mirror wrong reflection:
```
Get BPC_PlanarCapture → Call ActivateHorrorReflection()
Wait (duration from curve) → Call DeactivateHorrorReflection()
```
3. To boost a specific mirror for a scare moment:
```
Get BPC_PlanarCapture → Call SetScriptedPriority(1.0)
Wait (scare duration) → Call SetScriptedPriority(0.0)
```
## 8. Build Checklist
- [ ] C++ component compiled and functional
- [ ] No BP child needed — attach directly to actor
- [ ] Configure QualityProfiles array in component defaults (4 entries)
- [ ] Set CaptureMode in component defaults
- [ ] Assign SurfaceMeshComponent reference

View File

@@ -0,0 +1,125 @@
# 137 — Planar Capture Actor (`BP_PlanarCaptureActor`)
## Purpose
Placeable actor wrapping a `BPC_PlanarCapture` component. Owns the surface mesh, material dynamic instances, proximity trigger for quality scoring, and the capture component. Blueprint children (139-143) extend this for specific modes.
## Dependencies
- **Requires:** `BPC_PlanarCapture` (136), `SS_PlanarCaptureManager` (138)
- **Required By:** `BP_Mirror` (139), `BP_Portal` (140), `BP_Monitor` (141), `BP_HorrorMirror` (142), `BP_FakeWindow` (143)
- **Engine/Plugin Requirements:** Renderer module
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `Actor` (C++ `ABP_PlanarCaptureActor`) |
| **Class Type** | Blueprint Actor |
| **Asset Path** | `Content/Framework/Capture/` |
| **C++ Header** | `Source/PG_Framework/Public/Capture/BP_PlanarCaptureActor.h` |
| **C++ Status** | ✅ Full Implementation |
| **BP Asset** | BP children: `BP_Mirror`, `BP_Portal`, `BP_Monitor`, `BP_HorrorMirror`, `BP_FakeWindow` |
## 1. Components
| Component | Type | Description |
|-----------|------|-------------|
| `Root` | SceneComponent | Root transform |
| `SurfaceMesh` | StaticMeshComponent | Surface plane/quad mesh, collision enabled |
| `ProximityTrigger` | BoxComponent | Overlap volume for player proximity scoring (500x500x500 default) |
| `CaptureComponent` | UBPC_PlanarCapture | Core capture logic |
## 2. Variables
### Configuration
| Variable | Type | Default | Category | Description |
|----------|------|---------|----------|-------------|
| `SurfaceDisplayName` | FString | "" | `Capture\|Config` | Debug/display name |
| `bStartEnabled` | bool | true | `Capture\|Config` | Start active on BeginPlay |
| `bDestructible` | bool | false | `Capture\|Config` | Can be destroyed (shattered mirror) |
| `SurfaceMPC` | UMaterialParameterCollection* | — | `Capture\|Material` | Global MPC reference |
### Runtime
| Variable | Type | Description |
|----------|------|-------------|
| `bIsActive` | bool | Current active state |
| `SurfaceMaterialInstance` | UMaterialInstanceDynamic* | Created dynamic material instance |
### Replicated
| Variable | Type | Condition | Description |
|----------|------|-----------|-------------|
| `bRepIsActive` | bool | ReplicatedUsing OnRep_IsActive | Server-authoritative active state |
## 3. Functions
#### `EnableSurface()`
- **Description:** Activates the surface, initializes capture component
- **Flow:** Set bIsActive=true → CaptureComponent.InitializeCapture()
#### `DisableSurface()`
- **Description:** Deactivates the surface, shuts down capture
- **Flow:** Set bIsActive=false → CaptureComponent.ShutdownCapture()
#### `SetCaptureMode(NewMode: EPlanarCaptureMode)`
- **Description:** Change capture mode at runtime
- **Flow:** Shutdown → Change CaptureComponent.CaptureMode → Re-initialize → Broadcast OnModeChanged
#### `SetSurfaceMaterial(NewMaterial: UMaterialInterface*)`
- **Description:** Swap surface material at runtime (clean ↔ dirty mirror, etc.)
- **Flow:** SurfaceMesh.CreateDynamicMaterialInstance(NewMaterial) → Store as SurfaceMaterialInstance
#### `SetSurfaceMPCParameter(ParameterName: FName, Value: float)`
- **Description:** Set a single MPC scalar parameter on the surface's MID
#### `DestroySurface()`
- **Description:** Destroy the surface (shatter mirror, break monitor). Respects bDestructible flag.
- **Flow:** DisableSurface → Broadcast OnSurfaceDestroyed → (BP child handles visual FX)
## 4. Event Dispatchers
| Dispatcher | Parameters | Description |
|------------|-----------|-------------|
| `OnModeChanged` | NewMode: EPlanarCaptureMode | Mode changed at runtime |
| `OnSurfaceDestroyed` | Surface: ABP_PlanarCaptureActor* | Surface destroyed |
## 5. Overlap Events
| Event | Logic |
|-------|-------|
| `OnProximityBeginOverlap` | If OtherActor has Tag "Player" → CaptureComponent.SetScriptedPriority(0.3) |
| `OnProximityEndOverlap` | If OtherActor has Tag "Player" → CaptureComponent.SetScriptedPriority(0.0) |
## 6. Communication Matrix
| Target System | Method | What |
|---------------|--------|------|
| `SS_PlanarCaptureManager` | Direct (RegisterSurface) | Registers on BeginPlay |
| `BPC_PlanarCapture` | Direct (owns) | Full component access |
| `BPC_ScareEventSystem` (101) | Dispatcher / Interface | Horror mirror scare triggers |
| `SS_AudioManager` (132) | Direct | Surface audio (shatter, portal whoosh) |
## 7. Manual Implementation Guide
### 7.1 Blueprint Child Setup
1. Create Blueprint Class: Parent = `BP_PlanarCaptureActor`, Name = `BP_Mirror` (or `BP_Portal`, etc.)
2. Assign a plane/quad StaticMesh to `SurfaceMesh` in Components panel
3. Set `CaptureComponent.CaptureMode` in Class Defaults
4. Assign `SurfaceMPC` to `MPC_CaptureSurface`
5. Assign material to SurfaceMesh slot 0
### 7.2 Proximity Trigger Adjustments
- Resize `ProximityTrigger.BoxExtent` per surface (larger mirrors = larger trigger)
- The trigger boosts priority when player is near — adjust size for desired quality bump radius
### 7.3 Destruction Handling
```
Event OnSurfaceDestroyed
→ Play shatter sound via SS_AudioManager.PlaySoundAtLocation()
→ Spawn particle effect at actor location
→ (Optional) Notify BPC_ScareEventSystem for jump scare
→ (Optional) Set narrative flag via BPC_NarrativeStateSystem
```
## 8. Build Checklist
- [ ] C++ base class compiles
- [ ] Create BP child with correct mesh
- [ ] Assign material and MPC
- [ ] Configure CaptureComponent defaults
- [ ] Adjust proximity trigger size
- [ ] Test BeginPlay registration with manager
- [ ] Test Enable/Disable surface
- [ ] Test proximity overlap quality boost

View File

@@ -0,0 +1,108 @@
# 138 — Planar Capture Manager Subsystem (`SS_PlanarCaptureManager`)
## Purpose
Global budget manager for all planar capture surfaces in the world. One instance per World (World Subsystem). Scores every registered surface each frame and assigns quality tiers to stay within configurable budget limits. Owns the shared render target pool.
## Dependencies
- **Requires:** `BP_PlanarCaptureActor` (137) for surface registration, `BPC_PlanarCapture` (136) for tier assignment
- **Required By:** All capture surfaces in the world
- **Engine/Plugin Requirements:** WorldSubsystem (auto-created)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `WorldSubsystem` (C++ `ASS_PlanarCaptureManager`) |
| **Class Type** | World Subsystem |
| **Asset Path** | N/A (auto-created by engine) |
| **C++ Header** | `Source/PG_Framework/Public/Capture/SS_PlanarCaptureManager.h` |
| **C++ Status** | ✅ Full Implementation |
| **BP Asset** | None — engine auto-creates |
## 1. Variables
### Configuration (Budget)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `GlobalQualityCap` | EPlanarCaptureQualityTier | High | Max quality tier for any surface |
| `MaxHeroSurfaces` | int32 | 1 | Max simultaneous Hero-tier captures |
| `MaxHighSurfaces` | int32 | 3 | Max simultaneous High-tier captures |
| `MaxMediumSurfaces` | int32 | 6 | Max simultaneous Medium-tier captures |
| `MaxTotalRenderTargetMemoryMB` | float | 128.0 | Total RT memory budget |
| `MaxCaptureDistance` | float | 10000.0 | Distance at which surface goes Off |
| `FullEvaluationInterval` | float | 0.5 | Seconds between full re-evaluation |
## 2. Functions
#### `RegisterSurface(Surface: ABP_PlanarCaptureActor*)`
- **Description:** Register a surface with the manager. Called by BP_PlanarCaptureActor.BeginPlay.
- **Flow:** Check duplicates → Add to RegisteredSurfaces → Log → Broadcast OnSurfaceRegistered → EvaluateAllSurfaces
#### `UnregisterSurface(Surface: ABP_PlanarCaptureActor*)`
- **Description:** Remove a surface. Called on EndPlay.
- **Flow:** Remove from RegisteredSurfaces → Log → Broadcast OnSurfaceUnregistered
#### `RequestRenderTarget(Size: int32)` → `UTextureRenderTarget2D*`
- **Description:** Get a render target from the pool (or allocate new). Returns nullptr if pool exhausted.
- **Flow:** Check pool for matching size + not in use → If found, mark in use → If not, CreateRenderTarget
#### `ReleaseRenderTarget(RT: UTextureRenderTarget2D*)`
- **Description:** Return a render target to the pool. Marks as free for reuse.
#### `ForceAllSurfacesToTier(Tier: EPlanarCaptureQualityTier)`
- **Description:** Override all surfaces to a specific tier (e.g., Hero during a cutscene mirror moment).
#### `ReleaseForceTier()`
- **Description:** Remove force override, resume normal scoring.
#### `GetNearestSurfaceOfMode(Mode, Location, MaxDistance)` → `ABP_PlanarCaptureActor*`
- **Description:** Find nearest surface of a given mode (e.g., find nearest portal for teleport).
## 3. Event Dispatchers
| Dispatcher | Parameters | Description |
|------------|-----------|-------------|
| `OnSurfaceRegistered` | Surface, TotalSurfaces | Surface added |
| `OnSurfaceUnregistered` | Surface, TotalSurfaces | Surface removed |
| `OnGlobalQualityCapChanged` | NewCap | Quality cap changed |
## 4. Scoring Algorithm (Internal)
```
CompositeScore = (ScreenCoverage × 0.5) + (FacingAngle × 0.3) + (DistanceFactor × 0.1) + (ScriptedPriority × 0.1)
Tier: Score ≥ 0.8 → Hero | ≥ 0.5 → High | ≥ 0.2 → Medium | > 0 → Low | 0 → Off
Budget enforcement: demote if tier count exceeds Max*Surfaces limits
Apply GlobalQualityCap: clamp tier to cap
```
## 5. Communication Matrix
| Target System | Method | What |
|---------------|--------|------|
| `BP_PlanarCaptureActor` | Direct (registry) | Surface tracking |
| `BPC_PlanarCapture` | Direct (ApplyQualityTier) | Tier assignment |
| `UTextureRenderTarget2D` | Direct (create/release) | RT pool management |
| Player Camera Manager | UGameplayStatics (indirect via Capture) | View transform for scoring |
## 6. Manual Implementation Guide
*No Blueprint implementation needed — the C++ subsystem auto-creates per world.*
### 6.1 Budget Tuning
Adjust the following in `Project Settings → Plugins → PG_Framework → Planar Capture` (or via Config in DefaultEngine.ini):
- **High-end GPU (RTX 4080+):** MaxHeroSurfaces=2, MaxHighSurfaces=5
- **Mid-range (RTX 3060):** MaxHeroSurfaces=1, MaxHighSurfaces=3
- **Console (PS5/Xbox):** MaxHeroSurfaces=1, MaxHighSurfaces=2, GlobalQualityCap=High
- **Low-end (Steam Deck):** MaxHeroSurfaces=0, MaxHighSurfaces=1, GlobalQualityCap=Medium
### 6.2 Debug Commands
```
SS_PlanarCaptureManager.GetSurfaceCount() → Blueprint: print count
SS_PlanarCaptureManager.GetPoolMemoryUsageMB() → Blueprint: print memory
ForceAllSurfacesToTier(Hero) → test all at max quality
ReleaseForceTier() → resume normal scoring
```
## 7. Build Checklist
- [ ] C++ subsystem compiles
- [ ] Auto-creates per world (no manual creation needed)
- [ ] Budget limits tested with multiple surfaces
- [ ] RT pool reuses targets (not leaking)
- [ ] Force tier override works for cutscenes

View File

@@ -0,0 +1,110 @@
# 139 — Mirror Surface Actor (`BP_Mirror`)
## Purpose
Standard planar mirror surface. Designer configures mode, dirt layers, steam parameters, and surface aging. Extends `BP_PlanarCaptureActor` with default mirror configuration.
## Dependencies
- **Requires:** `BP_PlanarCaptureActor` (137), `BPC_PlanarCapture` (136)
- **Required By:** `BP_HorrorMirror` (142) — extends this for horror features
- **Engine/Plugin Requirements:** M_CaptureSurface_Master material, MPC_CaptureSurface
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `BP_PlanarCaptureActor` (C++ `ABP_PlanarCaptureActor`) |
| **Class Type** | Blueprint Actor |
| **Asset Path** | `Content/Framework/Capture/BP_Mirror.uasset` |
| **C++ Status** | 🔵 BP Spec Only — extends C++ parent |
| **BP Asset** | Create in editor: `BP_Mirror` |
## 1. Configuration (Class Defaults)
| Variable | Value | Description |
|----------|-------|-------------|
| `CaptureComponent.CaptureMode` | `Mirror` | Fixed to Mirror mode |
| `bStartEnabled` | `true` | Active by default |
| `bDestructible` | `false` | Can be shattered (override per instance) |
| `SurfaceMPC` | `MPC_CaptureSurface` | Assign the global MPC |
### Mirror-Specific Properties (Set via `SetSurfaceMPCParameter`)
| Parameter | Default | Range | Description |
|-----------|---------|-------|-------------|
| `DirtOpacity` | 0.0 | 0.01.0 | Dirt/scratch layer intensity |
| `SteamIntensity` | 0.0 | 0.01.0 | Steam fog visibility |
| `CondensationFlow` | 0.0 | 0.01.0 | Condensation rivulet normal intensity |
| `SurfaceAge` | 0.0 | 0.01.0 | Oxidation/yellowing tint |
## 2. Material Setup
| Slot | Material Instance | Description |
|------|------------------|-------------|
| SurfaceMesh[0] | `MI_Mirror_Clean` | Default clean mirror |
Pre-configured material instances to assign per mirror:
- `MI_Mirror_Clean` — Pristine mirror, no effects
- `MI_Mirror_Dirty` — DirtOpacity preset at 0.4
- `MI_Mirror_Steam` — SteamIntensity preset at 0.6, animated noise enabled
## 3. Event Graph
### Event BeginPlay
```
Event BeginPlay
├─ Parent: BeginPlay (registers with manager)
├─ Set CaptureComponent.CaptureMode = Mirror
├─ Set SurfaceMaterial(MI_Mirror_Clean) // or designer-chosen MI
└─ [If bStartEnabled] EnableSurface()
```
### Event: SetMirrorDirtLevel(Level: float)
```
SetMirrorDirtLevel(Level)
├─ Clamp Level to 0.01.0
├─ Call SetSurfaceMPCParameter("DirtOpacity", Level)
└─ [If Level > threshold] Swap material to MI_Mirror_Dirty
```
### Event: SetMirrorSteamLevel(Level: float)
```
SetMirrorSteamLevel(Level)
├─ Clamp Level to 0.01.0
├─ Call SetSurfaceMPCParameter("SteamIntensity", Level)
└─ Call SetSurfaceMPCParameter("CondensationFlow", Level * 0.5)
```
## 4. Communication Matrix
| Target | Method | What |
|--------|--------|------|
| `SS_PlanarCaptureManager` | Direct (inherited) | Registration |
| `BPC_PlanarCapture` | Direct (inherited) | Capture driving |
| `MPC_CaptureSurface` | SetScalarParameterValue (via PushMPCParameters) | Visual parameters |
| `SS_AudioManager` (132) | Direct (on shatter) | Shatter sound |
## 5. Manual Implementation Guide
### 5.1 Create BP_Mirror
1. Create Blueprint Class: Parent = `BP_PlanarCaptureActor`, Name = `BP_Mirror`
2. Open Class Defaults, set `CaptureComponent.CaptureMode` to `Mirror`
3. In Components: select `SurfaceMesh`, assign a plane mesh (e.g., `SM_Plane_200x200`)
4. Assign `MI_Mirror_Clean` to SurfaceMesh material slot 0
5. Set `SurfaceMPC` to `MPC_CaptureSurface`
### 5.2 Add Steam/Dirt Controls
1. In Event Graph, create custom event `SetMirrorDirtLevel(Float)`
2. Wire: `SetSurfaceMPCParameter("DirtOpacity", Level)`
3. Create custom event `SetMirrorSteamLevel(Float)`
4. Wire: `SetSurfaceMPCParameter("SteamIntensity", Level)` + `SetSurfaceMPCParameter("CondensationFlow", Level * 0.5)`
### 5.3 Level Placement
1. Drag `BP_Mirror` into level
2. Rotate so surface normal faces toward player viewing area
3. Set `SurfaceDisplayName` for debug identification
4. Adjust `ProximityTrigger.BoxExtent` to cover expected viewing area
## 6. Build Checklist
- [ ] Create BP_Mirror Blueprint child
- [ ] Assign plane mesh and MI_Mirror_Clean material
- [ ] Set CaptureMode=Mirror in defaults
- [ ] Assign MPC_CaptureSurface reference
- [ ] Test: place in level, walk up, verify reflection appears
- [ ] Test: walk away, verify quality drops to Off
- [ ] Test: call SetMirrorDirtLevel/SetMirrorSteamLevel from gameplay code

View File

@@ -0,0 +1,124 @@
# 140 — Portal Surface Actor (`BP_Portal`)
## Purpose
Portal surface with linked target. Renders the view from another location in the world through a planar surface. Supports one-way vs two-way rendering, teleport on overlap, and clip plane for flush geometry placement.
## Dependencies
- **Requires:** `BP_PlanarCaptureActor` (137), `BPC_PlanarCapture` (136), `BPC_StateManager` (130) for teleport gating
- **Required By:** Narrative sequences, level transitions, horror portal variants
- **Engine/Plugin Requirements:** Oblique clip plane requires Renderer module (C++)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `BP_PlanarCaptureActor` (C++ `ABP_PlanarCaptureActor`) |
| **Class Type** | Blueprint Actor |
| **Asset Path** | `Content/Framework/Capture/BP_Portal.uasset` |
| **C++ Status** | 🔵 BP Spec Only |
| **BP Asset** | Create in editor: `BP_Portal` |
## 1. Configuration (Class Defaults)
| Variable | Value | Description |
|----------|-------|-------------|
| `CaptureComponent.CaptureMode` | `Portal` | Fixed to Portal mode |
| `CaptureComponent.LinkedTargetSurface` | (Set per instance) | Destination portal actor |
| `bStartEnabled` | `true` | Active by default |
| `SurfaceMPC` | `MPC_CaptureSurface` | Global MPC |
### Portal-Specific Properties (Custom Variables — add to BP)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `bTeleportOnOverlap` | bool | true | Teleport player when they enter the portal volume |
| `bOneWay` | bool | true | True = only source→target; false = bidirectional |
| `TeleportTrigger` | BoxComponent | — | Overlap volume for teleport (separate from ProximityTrigger) |
## 2. Material Setup
| Slot | Material | Description |
|------|----------|-------------|
| SurfaceMesh[0] | `MI_Portal_Standard` | Portal surface with edge fade |
| Frame (optional) | Designer mesh + material | Portal frame decoration |
## 3. Event Graph
### Event BeginPlay
```
Event BeginPlay
├─ Parent: BeginPlay (registers with manager)
├─ Set CaptureComponent.CaptureMode = Portal
├─ Validate CaptureComponent.LinkedTargetSurface != nullptr
│ └─ If null → Log Warning: "BP_Portal missing LinkedTargetSurface!"
├─ Bind TeleportTrigger.OnComponentBeginOverlap → OnPortalOverlap
└─ EnableSurface()
```
### Event: OnPortalOverlap(OtherActor, OtherComp, ...)
```
OnPortalOverlap(OtherActor)
├─ Cast OtherActor to Player Pawn
├─ [If bTeleportOnOverlap and HasAuthority]
│ ├─ Query BPC_StateManager.IsActionPermitted(Framework.Action.Teleport)
│ │ └─ If denied → return (play denied feedback)
│ ├─ Get LinkedTargetSurface actor transform
│ ├─ Compute exit location: TargetTransform + offset
│ ├─ Teleport player: SetActorLocation + SetActorRotation
│ └─ Play portal travel SFX via SS_AudioManager
└─ [If client] Call Server_TeleportThroughPortal RPC
```
### Server RPC: Server_TeleportThroughPortal
```
Server_TeleportThroughPortal (Server, Reliable)
├─ Validate HasAuthority
├─ Validate portal still active
├─ Validate LinkedTargetSurface still valid
├─ Teleport player (same as OnPortalOverlap logic)
└─ Multicast_OnPlayerTeleported to notify all clients
```
## 4. Communication Matrix
| Target | Method | What |
|--------|--------|------|
| `BPC_StateManager` (130) | `IsActionPermitted()` | Teleport gating |
| `SS_AudioManager` (132) | Direct | Portal whoosh/disappear SFX |
| `SS_EnhancedInputManager` (128) | Direct (optional) | Context switch during portal transition |
| `BPC_CameraStateLayer` (14) | Direct (optional) | Camera FOV transition during teleport |
## 5. Manual Implementation Guide
### 5.1 Create BP_Portal
1. Create Blueprint Class: Parent = `BP_PlanarCaptureActor`, Name = `BP_Portal`
2. In Components: add `BoxComponent` named `TeleportTrigger`
3. Size `TeleportTrigger` slightly larger than surface mesh (50-100 units deep)
4. Set `TeleportTrigger.CollisionEnabled = QueryOnly`, response to Pawn = Overlap
5. Set `CaptureComponent.CaptureMode = Portal` in Class Defaults
6. Assign `MI_Portal_Standard` to SurfaceMesh
### 5.2 Portal Link Setup
1. Place `BP_Portal_Source` and `BP_Portal_Target` in level
2. On Source: set `CaptureComponent.LinkedTargetSurface` → Target actor
3. On Target: set `CaptureComponent.LinkedTargetSurface` → Source actor (if two-way)
4. Position surfaces facing each other's expected viewing directions
### 5.3 Teleport Implementation
```
Create custom event OnPortalOverlap (bind to TeleportTrigger)
→ Switch HasAuthority
Authority:
→ Get LinkedTargetSurface → Get Actor Transform
→ Teleport: Set Actor Location (target location + forward * 200)
→ Set Actor Rotation (target rotation)
→ Play Sound 2D (portal whoosh cue via SS_AudioManager)
Remote:
→ Call Server_TeleportThroughPortal (RPC)
```
## 6. Build Checklist
- [ ] Create BP_Portal with TeleportTrigger component
- [ ] Set CaptureMode=Portal
- [ ] Assign MI_Portal_Standard material
- [ ] Implement overlap teleport logic with HasAuthority gate
- [ ] Add Server_TeleportThroughPortal RPC
- [ ] Test one-way portal: source renders target view
- [ ] Test teleport: player overlaps and appears at target
- [ ] Test with BPC_StateManager gating (block teleport during certain states)

View File

@@ -0,0 +1,129 @@
# 141 — Monitor Surface Actor (`BP_Monitor`)
## Purpose
Security screen, TV, or diegetic display monitor. Uses a fixed camera actor reference instead of dynamic mirror/portal math. Runs at low FPS by default (5fps) with scanline and static noise effects.
## Dependencies
- **Requires:** `BP_PlanarCaptureActor` (137), `BPC_PlanarCapture` (136)
- **Required By:** `BPC_DiegeticDisplay` (18) — can display diegetic HUD content
- **Engine/Plugin Requirements:** None additional
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `BP_PlanarCaptureActor` (C++ `ABP_PlanarCaptureActor`) |
| **Class Type** | Blueprint Actor |
| **Asset Path** | `Content/Framework/Capture/BP_Monitor.uasset` |
| **C++ Status** | 🔵 BP Spec Only |
| **BP Asset** | Create in editor: `BP_Monitor` |
## 1. Configuration (Class Defaults)
| Variable | Value | Description |
|----------|-------|-------------|
| `CaptureComponent.CaptureMode` | `Monitor` | Fixed to Monitor mode |
| `CaptureComponent.CaptureFOV` | `70.0` | Narrower FOV for security camera feel |
| `CaptureComponent.QualityProfiles[0].CaptureInterval` | `0.2` | 5fps Low quality (override) |
| `SurfaceMPC` | `MPC_CaptureSurface` | Global MPC |
### Monitor-Specific Properties (Add to BP)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `bPoweredOn` | bool | true | On/off state |
| `bShowScanlines` | bool | false | CRT scanline overlay |
| `StaticNoiseIntensity` | float | 0.0 | Static/noise overlay (0.01.0) |
| `FlickerCurve` | UCurveFloat | — | On/off flicker timing curve |
| `PoweredOffMaterial` | UMaterialInterface | — | Material shown when off (black screen, cracked) |
## 2. Material Setup
| Slot | Material | Description |
|------|----------|-------------|
| SurfaceMesh[0] | `MI_Monitor_Security` | Security screen with scanline support |
## 3. Event Graph
### Event BeginPlay
```
Event BeginPlay
├─ Parent: BeginPlay
├─ Set CaptureComponent.CaptureMode = Monitor
├─ Set CaptureComponent.CaptureFOV = 70.0
├─ [If bPoweredOn] EnableSurface
└─ [Else] SetSurfaceMaterial(PoweredOffMaterial)
```
### Event: PowerOn
```
PowerOn()
├─ Set bPoweredOn = true
├─ SetSurfaceMaterial(MI_Monitor_Security)
├─ EnableSurface()
└─ [If FlickerCurve valid] StartFlickerTimeline
```
### Event: PowerOff
```
PowerOff()
├─ Set bPoweredOn = false
├─ DisableSurface()
├─ SetSurfaceMaterial(PoweredOffMaterial)
└─ StopFlickerTimeline
```
### Event: SetStaticNoise(Intensity: float)
```
SetStaticNoise(Intensity)
├─ Clamp Intensity 0.01.0
├─ Set StaticNoiseIntensity = Intensity
└─ SetSurfaceMPCParameter("StaticNoiseIntensity", Intensity)
```
## 4. Flicker Timeline Setup
If `FlickerCurve` is assigned, create a Timeline that drives:
```
Timeline: FlickerTimeline
Curve: FlickerCurve (Float Track)
On Update:
├─ Read curve value → bIsFlickerOn
├─ If bIsFlickerOn and not bPoweredOn → PowerOn (brief)
└─ If not bIsFlickerOn and bPoweredOn → PowerOff (brief)
```
## 5. Communication Matrix
| Target | Method | What |
|--------|--------|------|
| `BPC_DiegeticDisplay` (18) | Interface (I_DiegeticDisplay) | Can display diegetic HUD content as secondary output |
| `SS_AudioManager` (132) | Direct | Monitor static/buzz/hum SFX |
| `BPC_ScareEventSystem` (101) | Dispatcher | Monitor flicker/horror image scare |
## 6. Manual Implementation Guide
### 6.1 Create BP_Monitor
1. Create Blueprint Class: Parent = `BP_PlanarCaptureActor`, Name = `BP_Monitor`
2. Set `CaptureComponent.CaptureMode = Monitor` in defaults
3. Set `CaptureComponent.CaptureFOV = 70.0`
4. Override QualityProfiles[0].CaptureInterval to 0.2 (5fps Low)
5. Assign `MI_Monitor_Security` to SurfaceMesh
6. Create `PoweredOffMaterial` — simple dark/black emissive material
### 6.2 Camera Placement
1. Create a `CameraActor` or `SceneCaptureCamera` (BP child of CameraActor) in the level
2. Position it where the monitor should "look from"
3. In BP_Monitor instance: set `CaptureComponent.FixedCameraActor` → your camera actor
### 6.3 Power Control
For narrative-driven power control:
```
External system calls: BP_Monitor → PowerOn() / PowerOff()
Trigger: Narrative flag set → Branch → PowerOn or PowerOff
```
## 7. Build Checklist
- [ ] Create BP_Monitor Blueprint child
- [ ] Assign MI_Monitor_Security material
- [ ] Create PoweredOffMaterial (dark screen)
- [ ] Place camera actor and assign FixedCameraActor
- [ ] Test PowerOn/PowerOff cycle
- [ ] Test flicker curve timeline
- [ ] Test static noise parameter
- [ ] Test: place multiple monitors, verify quality budget works

View File

@@ -0,0 +1,162 @@
# 142 — Horror Mirror Actor (`BP_HorrorMirror`)
## Purpose
Horror mirror extending `BP_Mirror` with wrong-reflection Metahuman, delayed frame ring buffer, steam text reveal, mirror darkness, UV distortion, and coordinated scare events.
## Dependencies
- **Requires:** `BP_Mirror` (139), `BP_PlanarCaptureActor` (137), `BPC_PlanarCapture` (136)
- **Requires (Horror Integration):** `BPC_ScareEventSystem` (101), `SS_AudioManager` (132), `BPC_StressSystem` (10)
- **Engine/Plugin Requirements:** M_CaptureSurface_Master material (horror layers)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `BP_Mirror` (child of `BP_PlanarCaptureActor`) |
| **Class Type** | Blueprint Actor |
| **Asset Path** | `Content/Framework/Capture/BP_HorrorMirror.uasset` |
| **C++ Status** | 🔵 BP Spec Only — uses C++ horror API (ActivateHorrorReflection, PushDelayedFrame) |
| **BP Asset** | Create in editor: `BP_HorrorMirror` |
## 1. Configuration
| Variable | Value | Description |
|----------|-------|-------------|
| `CaptureComponent.CaptureMode` | `HorrorMirror` | Fixed to HorrorMirror mode |
| `CaptureComponent.WrongReflectionActor` | (Set per instance) | Metahuman/ghost actor for wrong reflection |
| `CaptureComponent.QualityProfiles[3].DelayedFrameCount` | `5` | Hero tier gets 5-frame delay |
### Horror-Specific Properties (Add to BP)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `WrongReflectionBlendCurve` | UCurveFloat | — | 0→1→0 blend curve for wrong reflection event |
| `MirrorDarknessCurve` | UCurveFloat | — | 0→1 darkness ramp curve |
| `UVDistortionCurve` | UCurveFloat | — | Distortion amplitude over time |
| `SteamTextRevealTexture` | UTexture2D | — | Mask texture for "HELP ME" text in steam |
| `ScareEventTag` | FGameplayTag | — | Tag identifying this scare in BPC_ScareEventSystem |
| `HorrorAudioCue` | USoundBase | — | Audio cue for horror event (played via SS_AudioManager) |
| `ScareDuration` | float | 5.0 | Total duration of the scare event |
## 2. Material Setup
| Slot | Material | Description |
|------|----------|-------------|
| SurfaceMesh[0] | `MI_Mirror_Horror` | Horror mirror with wrong reflection and darkness layers enabled |
## 3. Event Graph
### Event BeginPlay
```
Event BeginPlay
├─ Parent: BeginPlay (from BP_Mirror)
├─ Set CaptureComponent.CaptureMode = HorrorMirror
├─ Validate CaptureComponent.WrongReflectionActor != nullptr
│ └─ If null → Log Warning: "BP_HorrorMirror missing WrongReflectionActor!"
└─ EnableSurface()
```
### Core Event: TriggerHorrorScare()
```
TriggerHorrorScare()
├─ [Check] BPC_StateManager.IsActionPermitted(Scare.Tag) → if denied, return
├─ [Notify] BPC_ScareEventSystem.TriggerScareEvent(ScareEventTag)
│ └─ This coordinates with other systems (lights, audio, pacing)
├─ [Audio] SS_AudioManager.PlaySoundAtLocation(HorrorAudioCue, GetActorLocation())
├─ [Reflection Swap] CaptureComponent.ActivateHorrorReflection()
│ └─ C++ swaps ShowOnly list to WrongReflectionActor
├─ [Start Timeline] Play HorrorScareTimeline
│ └─ Timeline drives: WrongReflectionBlend, MirrorDarkness, UVDistortion, TextReveal
└─ [After ScareDuration] Call FinalizeHorrorScare()
```
### Timeline: HorrorScareTimeline
```
HorrorScareTimeline (Float Track, Length = ScareDuration)
Track 0: WrongReflectionBlend
Curve: WrongReflectionBlendCurve (0→1→0)
→ SetSurfaceMPCParameter("WrongReflectionBlend", curve value)
Track 1: MirrorDarkness
Curve: MirrorDarknessCurve (0→0.8→0.2 linger)
→ SetSurfaceMPCParameter("MirrorDarkness", curve value)
Track 2: UVDistortion
Curve: UVDistortionCurve (0→0.3 breath cycle)
→ SetSurfaceMPCParameter("DistortionAmplitude", curve value)
Track 3: TextReveal
Curve: 0→1 after 1.5s delay
→ SetSurfaceMPCParameter("TextRevealProgress", curve value)
```
### Event: FinalizeHorrorScare()
```
FinalizeHorrorScare()
├─ CaptureComponent.DeactivateHorrorReflection()
│ └─ C++ restores original ShowOnly list
├─ SetSurfaceMPCParameter("WrongReflectionBlend", 0.0)
├─ SetSurfaceMPCParameter("MirrorDarkness", 0.0)
├─ SetSurfaceMPCParameter("DistortionAmplitude", 0.0)
├─ SetSurfaceMPCParameter("TextRevealProgress", 0.0)
└─ [Notify] BPC_ScareEventSystem.OnScareEventComplete(ScareEventTag)
```
## 4. Communication Matrix
| Target | Method | What |
|--------|--------|------|
| `BPC_ScareEventSystem` (101) | Direct (TriggerScareEvent) | Coordinated scare |
| `SS_AudioManager` (132) | Direct | Horror SFX |
| `BPC_StressSystem` (10) | Dispatcher (optional) | Add stress on scare |
| `BPC_StateManager` (130) | IsActionPermitted() | Gate before triggering scare |
| `BPC_LightEventController` (96) | Dispatcher (optional) | Sync lights with mirror scare |
| `BPC_PacingDirector` (98) | Dispatcher (optional) | Notify of scare for pacing |
## 5. Manual Implementation Guide
### 5.1 Create BP_HorrorMirror
1. Create Blueprint Class: Parent = `BP_Mirror`, Name = `BP_HorrorMirror`
2. Set `CaptureComponent.CaptureMode = HorrorMirror` in defaults
3. Assign `MI_Mirror_Horror` to SurfaceMesh
4. Set `CaptureComponent.QualityProfiles[3].DelayedFrameCount = 5`
### 5.2 Setup Wrong Reflection Actor
1. Place a skeletal mesh actor (Metahuman, ghost mesh) in a hidden room/box near the mirror
2. Set its visibility to false in BeginPlay
3. In BP_HorrorMirror instance: set `CaptureComponent.WrongReflectionActor` → your hidden actor
### 5.3 Create Curves
Create Float Curve assets:
- `Curve_WrongReflectionBlend`: Keys: (0,0), (1.0,1.0), (3.0,1.0), (4.0,0), (5.0,0)
- `Curve_MirrorDarkness`: Keys: (0,0), (0.5,0.8), (2.0,0.6), (4.0,0.3), (5.0,0)
- `Curve_UVDistortion`: Keys: (0,0), (1.0,0.3), (2.0,0.2), (3.0,0.35), (5.0,0)
### 5.4 Build Timeline
1. Create Timeline node with Float Track
2. Add 4 float output tracks
3. Wire each track to `SetSurfaceMPCParameter` with corresponding param name
4. Set Timeline length to `ScareDuration`
### 5.5 Integration Hook
From any gameplay system (narrative trigger, AI event, proximity):
```
BP_HorrorMirror → TriggerHorrorScare()
```
From Sequencer:
```
Event Track → BP_HorrorMirror → TriggerHorrorScare()
```
## 6. Build Checklist
- [ ] Create BP_HorrorMirror Blueprint child
- [ ] Place WrongReflectionActor in level (hidden)
- [ ] Create all 3 Float Curves
- [ ] Build HorrorScareTimeline with 4 tracks
- [ ] Wire MPC parameter sets
- [ ] Wire BPC_ScareEventSystem integration
- [ ] Wire SS_AudioManager audio cue
- [ ] Test TriggerHorrorScare event
- [ ] Verify wrong reflection appears and fades
- [ ] Verify MPC params return to zero after scare

View File

@@ -0,0 +1,109 @@
# 143 — Fake Window Actor (`BP_FakeWindow`)
## Purpose
Architectural fake window using planar capture to simulate an outdoor view or adjacent room. Supports parallax depth, sky environment reference, weather overlay, and sublevel visibility.
## Dependencies
- **Requires:** `BP_PlanarCaptureActor` (137), `BPC_PlanarCapture` (136)
- **Engine/Plugin Requirements:** Level Streaming (for sublevel reference)
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `BP_PlanarCaptureActor` (C++ `ABP_PlanarCaptureActor`) |
| **Class Type** | Blueprint Actor |
| **Asset Path** | `Content/Framework/Capture/BP_FakeWindow.uasset` |
| **C++ Status** | 🔵 BP Spec Only |
| **BP Asset** | Create in editor: `BP_FakeWindow` |
## 1. Configuration
| Variable | Value | Description |
|----------|-------|-------------|
| `CaptureComponent.CaptureMode` | `FakeWindow` | Fixed to FakeWindow mode |
| `CaptureComponent.CaptureFOV` | `100.0` | Wider FOV for window view |
### FakeWindow-Specific Properties (Add to BP)
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `LinkedRoomSublevel` | TSoftObjectPtr〈UWorld〉 | — | Sublevel containing the room to render |
| `SkyReference` | AActor* | — | Sky/directional light to include in capture |
| `ParallaxDepthScalar` | float | 1.0 | Depth exaggeration for parallax effect |
| `WeatherOverlayType` | EWeatherType | None | Rain/Frost overlay driven by game state |
| `bStreamSublevelOnProximity` | bool | true | Load/unload sublevel based on player distance |
| `StreamInDistance` | float | 3000.0 | Distance to load sublevel |
## 2. Material Setup
| Slot | Material | Description |
|------|----------|-------------|
| SurfaceMesh[0] | `MI_FakeWindow_Interior` | Window with parallax and weather overlay support |
## 3. Event Graph
### Event BeginPlay
```
Event BeginPlay
├─ Parent: BeginPlay
├─ Set CaptureComponent.CaptureMode = FakeWindow
├─ [If bStreamSublevelOnProximity] Start distance check timer
└─ EnableSurface()
```
### Timer: CheckPlayerDistance
```
CheckPlayerDistance (every 2 seconds)
├─ Get Player Pawn location
├─ Distance = VectorDist(Player, GetActorLocation())
├─ If Distance < StreamInDistance AND sublevel not loaded
│ └─ Load Stream Level (LinkedRoomSublevel)
└─ If Distance > StreamInDistance + 1000 AND sublevel loaded
└─ Unload Stream Level (LinkedRoomSublevel)
```
### Event: SetWeatherOverlay(Weather: EWeatherType)
```
SetWeatherOverlay(Weather)
├─ Set WeatherOverlayType = Weather
├─ Switch on Weather:
│ None: SetSurfaceMPCParameter("RainIntensity", 0.0)
│ SetSurfaceMPCParameter("FrostIntensity", 0.0)
│ Rain: SetSurfaceMPCParameter("RainIntensity", 1.0)
│ Frost: SetSurfaceMPCParameter("FrostIntensity", 1.0)
└─ Trigger material update
```
## 4. Communication Matrix
| Target | Method | What |
|--------|--------|------|
| Level Streaming | `Load Stream Level` / `Unload Stream Level` | Sublevel management |
| `BPC_AtmosphereStateController` (94) | Dispatcher | React to environment state changes |
| `GS_CoreGameState` (6) | Direct (read) | Current chapter/phase for weather state |
## 5. Manual Implementation Guide
### 5.1 Create BP_FakeWindow
1. Create Blueprint Class: Parent = `BP_PlanarCaptureActor`, Name = `BP_FakeWindow`
2. Set `CaptureComponent.CaptureMode = FakeWindow`
3. Set `CaptureComponent.CaptureFOV = 100.0`
4. Assign `MI_FakeWindow_Interior` to SurfaceMesh
### 5.2 Setup the View Room
1. Create a separate sublevel with the room/exterior you want to show through the window
2. Add a `SceneCaptureCamera` actor in the sublevel positioned to capture the desired view
3. In BP_FakeWindow instance: assign `CaptureComponent.FixedCameraActor` → the camera in the sublevel
4. Assign `LinkedRoomSublevel` → the sublevel asset
### 5.3 Performance Considerations
- Fake windows should run at Low or Medium tier by default
- Sublevel streaming avoids rendering room geometry when player is far away
- Consider using a static cubemap fallback at Low quality instead of live capture
## 6. Build Checklist
- [ ] Create BP_FakeWindow Blueprint child
- [ ] Create sublevel with view room
- [ ] Place SceneCaptureCamera in sublevel
- [ ] Configure LinkedRoomSublevel and FixedCameraActor
- [ ] Create stream distance timer
- [ ] Set quality profiles for Low default tier
- [ ] Test sublevel streams in/out based on distance
- [ ] Test weather overlay switching

View File

@@ -0,0 +1,156 @@
# 144 — Capture Surface Master Material (`M_CaptureSurface_Master`)
## Purpose
Single master material for ALL capture surface modes (mirror, portal, monitor, horror, fake window). All features are parameter-driven — inactive features have zero rendering cost.
## Dependencies
- **Requires:** `MPC_CaptureSurface` (145) for global scalar parameters
- **Required By:** All capture surface actors (139-143) via their material instances
- **Engine/Plugin Requirements:** None
## Class Info
| Property | Value |
|----------|-------|
| **Material Domain** | Surface |
| **Blend Mode** | Opaque |
| **Shading Model** | Unlit |
| **Asset Path** | `Content/Framework/Capture/M_CaptureSurface_Master.uasset` |
| **C++ Status** | N/A (Material asset) |
| **Asset to Create** | Material in UE5 editor |
## 1. Material Layer Stack (9 Layers)
### Layer 0: Render Target Sample
```
Texture Sample (parameter: "CaptureTexture", TextureRenderTarget2D)
├─ UVs: If Mirror mode → UV * (1, -1) + (0, 1) [flip vertically for mirror reflection]
│ If Portal/Monitor mode → straight UVs
└─ Output → Base Color input of Layer 1
```
**Switch:** `StaticSwitchParameter: bIsMirror` (true = UV flip, false = straight)
### Layer 1: Condensation UV Distortion
```
Condensation Normal Map (Texture Sample, parameter: "CondensationNormal")
├─ UVs: Panner (slow drift, driven by Time * CondensationFlow MPC param)
├─ Normal strength: DistortionAmplitude MPC param
└─ Output: Modified UVs for Layer 0's texture sample
```
**Note:** Layer 1 modifies the UVs of Layer 0, not its color.
### Layer 2: Fresnel Edge Fade
```
Fresnel node (Exponent = 2.0, BaseReflectFraction = 0.0)
├─ Dot product: CameraVector · VertexNormal
└─ Output: Opacity mask for edge darkening (subtle vignette)
```
### Layer 3: Dirt / Scratch Multiply
```
Dirt Mask (Texture Sample, parameter: "DirtMask")
├─ Multiply with DirtOpacity MPC param
├─ Lerp: Clean RT ←→ Dirty RT using DirtOpacity
└─ Tint: Mix with SurfaceAge MPC param (yellowing/oxidation)
```
### Layer 4: Steam / Fog Lerp
```
Animated Noise (Procedural noise, tiling, scrolling via Time)
├─ SteamIntensity MPC param → controls noise opacity
├─ Lerp: Clear view ←→ Fogged view
└─ Output: blended base color
```
### Layer 5: Steam Emissive Glow
```
Steam mask (same noise as Layer 4, separate brightness)
├─ Multiply by SteamEmissiveIntensity MPC param
├─ Color: Soft warm white (backlit fog look)
└─ Output → Emissive Color pin
```
### Layer 6: Text Reveal Mask
```
Text Mask (Texture Sample, parameter: "TextRevealMask")
├─ Reveal progress: TextRevealProgress MPC param (0→1 wipe)
├─ Blend with steam carrier (appears as text IN the steam)
└─ Output: emissive text overlay (optional) or color replacement
```
### Layer 7: Mirror Darkness Multiply
```
MirrorDarkness MPC param (0.0 = normal, 1.0 = black)
├─ Multiply base color by (1.0 - MirrorDarkness)
└─ Output: darkened reflection
```
### Layer 8: Wrong Reflection Crossfade
```
Second RT Input (parameter: "DelayedCaptureTexture" OR separate RT)
├─ Lerp: Current_RT ←→ Delayed/Wrong_RT using WrongReflectionBlend MPC param
└─ Final Output → Emissive Color (Unlit material)
```
## 2. Material Parameters (Exposed)
### Texture Parameters
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `CaptureTexture` | TextureRenderTarget2D | — | Live capture render target |
| `CondensationNormal` | Texture2D | T_DefaultNormal | Condensation flow normal map |
| `DirtMask` | Texture2D | T_DefaultWhite | Dirt/scratch mask texture |
| `TextRevealMask` | Texture2D | T_DefaultBlack | Steam text mask |
### Scalar Parameters (Driven by MPC)
| Name | Default | Description |
|------|---------|-------------|
| `SteamIntensity` | 0.0 | Steam fog opacity |
| `DirtOpacity` | 0.0 | Dirt/scratch opacity |
| `CondensationFlow` | 0.0 | Condensation normal scroll speed |
| `DistortionAmplitude` | 0.0 | UV distortion magnitude |
| `MirrorDarkness` | 0.0 | Mirror darkening |
| `WrongReflectionBlend` | 0.0 | Wrong reflection crossfade |
| `TextRevealProgress` | 0.0 | Steam text reveal progress |
| `SteamEmissiveIntensity` | 0.0 | Steam glow brightness |
| `DelayedReflectionBlend` | 0.0 | Delayed frame blend |
| `SurfaceAge` | 0.0 | Oxidation/yellowing tint |
### Static Switch Parameters
| Name | Default | Description |
|------|---------|-------------|
| `bIsMirror` | true | Enable vertical UV flip for mirror mode |
| `bEnableHorrorLayers` | false | Enable Layers 6-8 (text reveal, darkness, wrong reflection) |
## 3. MPC Binding
All 10 scalar parameters are NOT material-instance parameters — they are driven by `MPC_CaptureSurface` (145) at runtime. The C++ component calls `PushMPCParameters()` each frame to update these values globally.
In the material: use `Collection Parameter` nodes referencing `MPC_CaptureSurface` for each of the 10 scalars listed above.
## 4. Manual Implementation Guide
### 4.1 Create Master Material
1. Create Material: Name = `M_CaptureSurface_Master`
2. Set Material Domain = Surface, Blend Mode = Opaque, Shading Model = Unlit
3. Build Layer 0: `Texture Sample Parameter` node named "CaptureTexture"
4. Add Static Switch: "bIsMirror" → UV flip branch (Custom UV × (1,-1) + (0,1))
5. Build Layers 1-8 as described above
6. Connect final color → Emissive Color (Unlit output)
### 4.2 MPC Integration
1. Create `Collection Parameter` nodes for each of the 10 MPC scalars
2. Set the Collection asset to `MPC_CaptureSurface`
3. Set the Parameter Name to match each MPC entry (e.g., "SteamIntensity")
### 4.3 Performance
- Use `StaticSwitchParameter` for `bEnableHorrorLayers` — when false, Layers 6-8 are compiled out (zero cost)
- Condensation normal Layer 1 is cheap (one texture sample + panner)
- All MPC reads are free (constant buffer lookup)
## 5. Build Checklist
- [ ] Create M_CaptureSurface_Master material in UE5 editor
- [ ] Set Domain=Surface, Blend=Opaque, Shading=Unlit
- [ ] Build all 9 layers (verify material compiles)
- [ ] Add 10 MPC Collection Parameter nodes
- [ ] Add bIsMirror and bEnableHorrorLayers static switches
- [ ] Compile material — no errors
- [ ] Create MI instances and verify parameter override works

View File

@@ -0,0 +1,72 @@
# 145 — Capture Surface Material Parameter Collection (`MPC_CaptureSurface`)
## Purpose
Global Material Parameter Collection with 10 scalar parameters that control all capture surface material effects at runtime. Pushed each frame by `UBPC_PlanarCapture::PushMPCParameters()`.
## Dependencies
- **Requires:** None (standalone MPC asset)
- **Required By:** `M_CaptureSurface_Master` (144), all capture surface actors (139-143)
- **Engine/Plugin Requirements:** None
## Class Info
| Property | Value |
|----------|-------|
| **Asset Type** | MaterialParameterCollection |
| **Asset Path** | `Content/Framework/Capture/MPC_CaptureSurface.uasset` |
| **C++ Status** | N/A (content asset) |
| **Asset to Create** | MPC in UE5 editor |
## 1. Scalar Parameters
| Parameter Name | Default Value | Range | Description |
|---------------|---------------|-------|-------------|
| `SteamIntensity` | 0.0 | 0.01.0 | Steam/fog opacity on mirror surface |
| `DirtOpacity` | 0.0 | 0.01.0 | Dirt/scratch layer opacity |
| `CondensationFlow` | 0.0 | 0.02.0 | Condensation normal map panning speed |
| `DistortionAmplitude` | 0.0 | 0.01.0 | UV distortion magnitude (breathing mirror) |
| `MirrorDarkness` | 0.0 | 0.01.0 | Mirror darkening (0=normal, 1=black) |
| `WrongReflectionBlend` | 0.0 | 0.01.0 | Wrong reflection crossfade (horror mode) |
| `TextRevealProgress` | 0.0 | 0.01.0 | Steam text reveal wipe progress |
| `SteamEmissiveIntensity` | 0.0 | 0.05.0 | Backlit fog emissive brightness |
| `DelayedReflectionBlend` | 0.0 | 0.01.0 | Delayed frame ring buffer blend |
| `SurfaceAge` | 0.0 | 0.01.0 | Oxidation / yellowing tint amount |
## 2. Vector Parameters (Optional — Legendary Tier)
| Parameter Name | Default Value | Description |
|---------------|---------------|-------------|
| `SurfaceTintColor` | (1.0, 1.0, 1.0, 1.0) | Oxidation/aging color tint |
| `SteamColor` | (0.8, 0.85, 0.9, 1.0) | Steam emissive color |
## 3. Runtime Control
All parameters are driven by C++ via `UBPC_PlanarCapture::PushMPCParameters()`. Blueprint actors call `SetSurfaceMPCParameter(Name, Value)` on the parent `BP_PlanarCaptureActor` to override specific values for that surface.
### Per-Surface Override Pattern
```
BP_HorrorMirror → SetSurfaceMPCParameter("WrongReflectionBlend", 1.0)
└─ This sets the scalar on the surface's MID, which overrides the MPC value
(MID overrides take priority over MPC in UE5 material system)
```
### Global Override Pattern
```
SS_PlanarCaptureManager → all surfaces get MPC pushed each frame
└─ A gameplay system changes MPC global → affects ALL mirrors/potals/monitors
```
## 4. Manual Implementation Guide
1. Create Material Parameter Collection: Name = `MPC_CaptureSurface`
2. Add all 10 scalar parameters with names and default values from Section 1
3. Add 2 optional vector parameters if using color tinting
4. Assign this MPC to every capture surface actor's `SurfaceMPC` variable
5. In `M_CaptureSurface_Master`, link all `Collection Parameter` nodes to this MPC
## 5. Build Checklist
- [ ] Create MPC_CaptureSurface in editor
- [ ] Add all 10 scalar parameters
- [ ] Add 2 vector parameters (optional)
- [ ] Assign to all BP_* capture actors' SurfaceMPC variable
- [ ] Verify material compiles with MPC references
- [ ] Test: change SteamIntensity in MPC, verify all mirrors fog

View File

@@ -0,0 +1,94 @@
# 146 — Planar Capture Profile Data Asset (`DA_PlanarCaptureProfile`)
## Purpose
Designer-configurable capture profile for individual surfaces. Defines default mode, quality profile overrides, actor lists, and surface material. Referenced by `BP_PlanarCaptureActor` at BeginPlay.
## Dependencies
- **Requires:** `PlanarCaptureCommon.h` (C++ enums/structs)
- **Required By:** All capture surface actors (139-143)
- **Engine/Plugin Requirements:** None
## Class Info
| Property | Value |
|----------|-------|
| **Parent Class** | `PrimaryDataAsset` (C++ may extend) |
| **Class Type** | Data Asset |
| **Asset Path** | `Content/Framework/Capture/DA_PlanarCaptureProfile.uasset` |
| **C++ Status** | N/A (Data Asset) |
| **Asset to Create** | Data Asset in UE5 editor |
## 1. Variables
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `DefaultMode` | EPlanarCaptureMode | Mirror | Default capture mode |
| `QualityProfileOverrides` | TArray〈FPlanarCaptureQualityProfile〉 | — | Override specific quality tier settings (0=Low, 1=Medium, 2=High, 3=Hero) |
| `DefaultShowOnlyActors` | TArray〈FPlanarCaptureActorListEntry〉 | — | Pre-configured ShowOnly actors |
| `DefaultHiddenActors` | TArray〈FPlanarCaptureActorListEntry〉 | — | Pre-configured Hidden actors |
| `bStartEnabled` | bool | true | Surface starts active |
| `bDestructible` | bool | false | Can be destroyed |
| `SurfaceMaterialOverride` | TSoftObjectPtr〈UMaterialInterface〉 | — | Override surface material |
| `SurfaceMPCOverride` | TSoftObjectPtr〈UMaterialParameterCollection〉 | — | Override MPC reference |
| `DefaultFOV` | float | 90.0 | Override capture FOV |
| `MaxViewDistance` | float | 5000.0 | Override max view distance |
## 2. Usage Pattern
### At BeginPlay (BP_PlanarCaptureActor)
```
Event BeginPlay
├─ If DA_PlanarCaptureProfile is assigned:
│ ├─ CaptureComponent.CaptureMode = Profile.DefaultMode
│ ├─ CaptureComponent.CaptureFOV = Profile.DefaultFOV
│ ├─ CaptureComponent.ShowOnlyActors = Profile.DefaultShowOnlyActors
│ ├─ CaptureComponent.HiddenActors = Profile.DefaultHiddenActors
│ ├─ bStartEnabled = Profile.bStartEnabled
│ ├─ bDestructible = Profile.bDestructible
│ ├─ [If SurfaceMaterialOverride] SetSurfaceMaterial(Override)
│ └─ [If SurfaceMPCOverride] SurfaceMPC = Override
└─ Continue with normal BeginPlay
```
This allows placing 20 BP_Mirror instances in a level, each referencing a different DA_PlanarCaptureProfile — no per-instance variable tweaking needed.
## 3. Profile Examples
### Profile: HeroMirror
| Variable | Value |
|----------|-------|
| DefaultMode | Mirror |
| QualityProfileOverrides[3] | Hero: RT 2048, 60fps, Full Lumen, Post |
| bStartEnabled | true |
| bDestructible | false |
### Profile: SecurityMonitor
| Variable | Value |
|----------|-------|
| DefaultMode | Monitor |
| DefaultFOV | 70.0 |
| QualityProfileOverrides[0] | Low: RT 256, 5fps |
| MaxViewDistance | 3000.0 |
### Profile: HorrorMirror_ScareReady
| Variable | Value |
|----------|-------|
| DefaultMode | HorrorMirror |
| QualityProfileOverrides[3] | Hero: RT 2048, 12fps, DelayedFrame=5 |
| bDestructible | true |
| SurfaceMaterialOverride | MI_Mirror_Horror |
## 4. Manual Implementation Guide
1. Create Data Asset: Parent = `PrimaryDataAsset`, Name = `DA_PlanarCaptureProfile`
2. Add variables as listed in Section 1 (create struct for QualityProfileOverrides if needed)
3. Create multiple profile instances per surface type
4. In each BP_* child, add variable: `CaptureProfile: DA_PlanarCaptureProfile` (Instance Editable, Expose on Spawn)
5. In BeginPlay override, read profile and apply all settings before EnableSurface()
## 5. Build Checklist
- [ ] Create DA_PlanarCaptureProfile Data Asset (or C++ struct)
- [ ] Add all 10 variables
- [ ] Create profile instances: DA_HeroMirror, DA_SecurityMonitor, DA_HorrorMirror
- [ ] Add CaptureProfile variable to BP_PlanarCaptureActor
- [ ] Wire BeginPlay to apply profile
- [ ] Test: place mirror with different profiles, verify settings apply

View File

@@ -0,0 +1,130 @@
# 147 — Capture Surface Material Instances (`MI_Mirror_*`, `MI_Portal_*`, `MI_Monitor_*`, `MI_FakeWindow_*`)
## Purpose
Pre-configured Material Instance Constants (MICs) for common capture surface states. These are children of `M_CaptureSurface_Master` (144) with static switches and default parameters pre-set for each use case.
## Dependencies
- **Requires:** `M_CaptureSurface_Master` (144), `MPC_CaptureSurface` (145)
- **Required By:** All capture surface actors (139-143)
- **Engine/Plugin Requirements:** None
## Class Info
| Property | Value |
|----------|-------|
| **Asset Type** | MaterialInstanceConstant |
| **Parent Material** | `M_CaptureSurface_Master` |
| **Asset Path** | `Content/Framework/Capture/Materials/` |
| **C++ Status** | N/A |
| **Assets to Create** | 7 material instances |
## 1. Material Instance Catalog
### MI_Mirror_Clean
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `true` |
| `bEnableHorrorLayers` | `false` |
| Scalar Override | Value | Note |
|-----------------|-------|------|
| `SteamIntensity` | 0.0 | No fog |
| `DirtOpacity` | 0.0 | Clean |
| `SurfaceAge` | 0.0 | No oxidation |
### MI_Mirror_Dirty
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `true` |
| `bEnableHorrorLayers` | `false` |
| Scalar Override | Value | Note |
|-----------------|-------|------|
| `DirtOpacity` | 0.4 | Moderate dirt |
| `SurfaceAge` | 0.3 | Slight yellowing |
### MI_Mirror_Steam
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `true` |
| `bEnableHorrorLayers` | `false` |
| Scalar Override | Value | Note |
|-----------------|-------|------|
| `SteamIntensity` | 0.6 | Visible fog |
| `CondensationFlow` | 0.5 | Slow rivulet drift |
| `DistortionAmplitude` | 0.2 | Slight warp |
| `SteamEmissiveIntensity` | 0.3 | Subtle glow |
### MI_Mirror_Horror
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `true` |
| `bEnableHorrorLayers` | `true` |
| Scalar Override | Value | Note |
|-----------------|-------|------|
| `WrongReflectionBlend` | 0.0 | Starts off (driven by MPC at runtime) |
| `MirrorDarkness` | 0.0 | Starts normal |
| `TextRevealProgress` | 0.0 | Hidden until scare |
| `DistortionAmplitude` | 0.0 | Normal until scare |
### MI_Portal_Standard
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `false` |
| `bEnableHorrorLayers` | `false` |
| Texture Override | Value | Note |
|------------------|-------|------|
| `CaptureTexture` | (set at runtime by component) | RT assigned dynamically |
### MI_Monitor_Security
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `false` |
| `bEnableHorrorLayers` | `false` |
| Scalar Override | Value | Note |
|-----------------|-------|------|
| `ScanlineIntensity` | 0.3 | CRT scanline overlay (add as custom parameter) |
### MI_FakeWindow_Interior
| Static Switch | Value |
|---------------|-------|
| `bIsMirror` | `false` |
| `bEnableHorrorLayers` | `false` |
| Scalar Override | Value | Note |
|-----------------|-------|------|
| `RainIntensity` | 0.0 | Weather overlay off (add as custom parameter) |
| `FrostIntensity` | 0.0 | Weather overlay off (add as custom parameter) |
## 2. Usage Assignment
| Actor | Default MI | Swap Options |
|-------|-----------|--------------|
| `BP_Mirror` | MI_Mirror_Clean | MI_Mirror_Dirty, MI_Mirror_Steam |
| `BP_Portal` | MI_Portal_Standard | — |
| `BP_Monitor` | MI_Monitor_Security | PoweredOffMaterial (custom) |
| `BP_HorrorMirror` | MI_Mirror_Horror | MI_Mirror_Clean (pre-scare), MI_Mirror_Steam |
| `BP_FakeWindow` | MI_FakeWindow_Interior | — |
## 3. Manual Implementation Guide
1. Create all 7 Material Instance Constants
2. Parent: `M_CaptureSurface_Master`
3. For each, set Static Switch Parameters as listed above
4. Set Scalar Parameter Overrides where non-zero
5. Assign to corresponding Blueprint actor defaults
6. Test: place mirror with MI_Mirror_Steam, verify fogged appearance
## 4. Build Checklist
- [ ] Create MI_Mirror_Clean (bIsMirror=true, all scalars=0)
- [ ] Create MI_Mirror_Dirty (DirtOpacity=0.4)
- [ ] Create MI_Mirror_Steam (SteamIntensity=0.6)
- [ ] Create MI_Mirror_Horror (bEnableHorrorLayers=true)
- [ ] Create MI_Portal_Standard (bIsMirror=false)
- [ ] Create MI_Monitor_Security (bIsMirror=false)
- [ ] Create MI_FakeWindow_Interior (bIsMirror=false)
- [ ] Assign defaults to each BP child's SurfaceMesh
- [ ] Verify all compile without material errors

View File

@@ -53,7 +53,8 @@ docs/blueprints/
├── 13-polish/ ← Tutorial, Loading, Credits, Debug (9 files)
├── 14-data-assets/ ← Data Asset definitions (16 files)
├── 15-input/ ← Enhanced Input System (1 file)
── 16-state/ ← State Management (2 files)
── 16-state/ ← State Management (2 files)
└── 17-capture/ ← Planar Capture System (12 files)
```
---
@@ -213,6 +214,19 @@ docs/blueprints/
| 133 | [`133_BP_RoomAudioZone`](10-adaptive/133_BP_RoomAudioZone.md) | `BP_` Actor | `TriggerVolume` | Room audio zone; auto-switches reverb on overlap via push/pop stack | 10-adaptive |
| 134 | [`134_DA_AudioSettings`](14-data-assets/134_DA_AudioSettings.md) | `DA_` Data Asset | `PrimaryDataAsset` | Audio bus configs, volume defaults, ducking, pool limits | 14-data-assets |
| 135 | [`135_DA_RoomAcousticPreset`](14-data-assets/135_DA_RoomAcousticPreset.md) | `DA_` Data Asset | `PrimaryDataAsset` | Room acoustic profile; 7 presets (Outdoor, SmallRoom, Cave, etc.) | 14-data-assets |
| — | — | — | — | — | — |
| 136 | [`136_BPC_PlanarCapture`](17-capture/136_BPC_PlanarCapture.md) | `BPC_` Component | `ActorComponent` | Core capture: SceneCapture2D lifecycle, camera math, RT management, horror ring buffer | 17-capture |
| 137 | [`137_BP_PlanarCaptureActor`](17-capture/137_BP_PlanarCaptureActor.md) | `BP_` Actor | `Actor` | Placeable surface actor: mesh, MDI, proximity trigger, manager registration | 17-capture |
| 138 | [`138_SS_PlanarCaptureManager`](17-capture/138_SS_PlanarCaptureManager.md) | `SS_` Subsystem | `WorldSubsystem` | Global budget manager: surface scoring, quality tier assignment, RT pool | 17-capture |
| 139 | [`139_BP_Mirror`](17-capture/139_BP_Mirror.md) | `BP_` Actor | `BP_PlanarCaptureActor` | Standard mirror: Mode=Mirror, dirt/steam/condensation layers, aging | 17-capture |
| 140 | [`140_BP_Portal`](17-capture/140_BP_Portal.md) | `BP_` Actor | `BP_PlanarCaptureActor` | Portal surface: Mode=Portal, linked target, teleport on overlap, clip plane | 17-capture |
| 141 | [`141_BP_Monitor`](17-capture/141_BP_Monitor.md) | `BP_` Actor | `BP_PlanarCaptureActor` | Security screen: Mode=Monitor, fixed camera, 5fps, scanlines | 17-capture |
| 142 | [`142_BP_HorrorMirror`](17-capture/142_BP_HorrorMirror.md) | `BP_` Actor | `BP_Mirror` | Horror mirror: wrong reflection, delayed frame, text reveal, scare events | 17-capture |
| 143 | [`143_BP_FakeWindow`](17-capture/143_BP_FakeWindow.md) | `BP_` Actor | `BP_PlanarCaptureActor` | Architectural fake window: Mode=FakeWindow, sublevel, weather overlay | 17-capture |
| 144 | [`144_M_CaptureSurface_Master`](17-capture/144_M_CaptureSurface_Master.md) | `M_` Material | `Material` | Master unlit material: 9-layer stack (RT, condensation, dirt, steam, horror) | 17-capture |
| 145 | [`145_MPC_CaptureSurface`](17-capture/145_MPC_CaptureSurface.md) | MPC | `MaterialParameterCollection` | Global MPC: 10 scalar params (SteamIntensity, DirtOpacity, MirrorDarkness, etc.) | 17-capture |
| 146 | [`146_DA_PlanarCaptureProfile`](17-capture/146_DA_PlanarCaptureProfile.md) | `DA_` Data Asset | `PrimaryDataAsset` | Per-surface capture config: mode, quality overrides, actor lists | 17-capture |
| 147 | [`147_MI_MirrorInstances`](17-capture/147_MI_MirrorInstances.md) | `MI_` Instances | `M_CaptureSurface_Master` | 7 pre-configured material instances (Clean/Dirty/Steam/Horror/Portal/Monitor/FakeWindow) | 17-capture |
---
@@ -233,7 +247,7 @@ docs/blueprints/
| `AI_` | AI Controller | 1 |
| `BB_` | Blackboard | 1 |
| `E_` | Enum | 5 |
| **Total** | | **146** |
| **Total** | | **158** |
---

View File

@@ -1,6 +1,6 @@
# C++ & Blueprint Status Checklist — All 135 Systems
# C++ & Blueprint Status Checklist — All 147 Systems
**Version:** 2.0 | **Generated:** 2026-05-21 | **C++ Files:** 22 `.h` + 22 `.cpp` (12 full implementations + 10 stubs)
**Version:** 2.1 | **Generated:** 2026-05-22 | **C++ Files:** 27 `.h` + 27 `.cpp` (15 full implementations + 10 stubs)
Complete status grid for every system in the UE5 Modular Game Framework. Use this to track: which systems have C++ code written, which Blueprint specs are complete, and what Blueprint assets remain to create.
@@ -213,6 +213,27 @@ Abbreviations:
---
## Phase 17: Planar Capture System (17-capture — 12 systems)
### Capture Components & Actors (17-capture)
| # | System | C++ | BP Spec | BP Asset |
|---|--------|-----|---------|----------|
| 136 | `BPC_PlanarCapture` | ✅ Full — `Capture/BPC_PlanarCapture` | ✅ | None (use C++ component directly) |
| 137 | `BP_PlanarCaptureActor` | ✅ Full — `Capture/BP_PlanarCaptureActor` | ✅ | BP children (139-143 extend this) |
| 138 | `SS_PlanarCaptureManager` | ✅ Full — `Capture/SS_PlanarCaptureManager` | ✅ | None (auto-created WorldSubsystem) |
| 139 | `BP_Mirror` | 🔵 BP-only | ✅ | BP actor child of BP_PlanarCaptureActor |
| 140 | `BP_Portal` | 🔵 BP-only | ✅ | BP actor child of BP_PlanarCaptureActor |
| 141 | `BP_Monitor` | 🔵 BP-only | ✅ | BP actor child of BP_PlanarCaptureActor |
| 142 | `BP_HorrorMirror` | 🔵 BP-only (extends BP_Mirror) | ✅ | BP actor child of BP_Mirror |
| 143 | `BP_FakeWindow` | 🔵 BP-only | ✅ | BP actor child of BP_PlanarCaptureActor |
| 144 | `M_CaptureSurface_Master` | ⬜ Content Asset | ✅ | Create Material in editor |
| 145 | `MPC_CaptureSurface` | ⬜ Content Asset | ✅ | Create MPC in editor |
| 146 | `DA_PlanarCaptureProfile` | 🔵 BP-only | ✅ | ⬜ Data Asset instances |
| 147 | `MI_MirrorInstances` | ⬜ Content Asset | ✅ | 7 Material Instance Constants |
---
## Quick Action List — What to Build Next
### Immediate (Phase 0 — Foundation)
@@ -261,7 +282,7 @@ Abbreviations:
## C++ Source File Index
### Full Implementations (12 systems — logic complete, no BP child needed for functionality)
### Full Implementations (15 systems — logic complete, no BP child needed for functionality)
| # | C++ Class | Header | Source | Size |
|---|-----------|--------|--------|------|
@@ -277,6 +298,9 @@ Abbreviations:
| 72 | `UBPC_DamageReceptionSystem` | `Public/Weapons/BPC_DamageReceptionSystem.h` | `Private/Weapons/BPC_DamageReceptionSystem.cpp` | 150 + cpp |
| 128 | `USS_EnhancedInputManager` | `Public/Input/SS_EnhancedInputManager.h` | `Private/Input/SS_EnhancedInputManager.cpp` | 232 + cpp |
| 130 | `UBPC_StateManager` | `Public/Player/BPC_StateManager.h` | `Private/Player/BPC_StateManager.cpp` | 246 + cpp |
| 136 | `UBPC_PlanarCapture` | `Public/Capture/BPC_PlanarCapture.h` | `Private/Capture/BPC_PlanarCapture.cpp` | 360 + cpp |
| 137 | `ABP_PlanarCaptureActor` | `Public/Capture/BP_PlanarCaptureActor.h` | `Private/Capture/BP_PlanarCaptureActor.cpp` | 250 + cpp |
| 138 | `ASS_PlanarCaptureManager` | `Public/Capture/SS_PlanarCaptureManager.h` | `Private/Capture/SS_PlanarCaptureManager.cpp` | 330 + cpp |
### C++ Stubs (10 systems — UCLASS + basic vars/delegates, need BP child for logic)
@@ -293,6 +317,26 @@ Abbreviations:
| 120 | `UDA_EquipmentConfig` | `Public/Inventory/DA_EquipmentConfig.h` | `Private/Inventory/DA_EquipmentConfig.cpp` | Equipment config stub |
| 131 | `UDA_StateGatingTable` | `Public/State/DA_StateGatingTable.h` | `Private/State/DA_StateGatingTable.cpp` | Gating table stub |
### Planar Capture Systems (3 full C++ implementations)
| # | C++ Class | Header | Source | Purpose |
|---|-----------|--------|--------|---------|
| 136 | `UBPC_PlanarCapture` | `Public/Capture/BPC_PlanarCapture.h` | `Private/Capture/BPC_PlanarCapture.cpp` | SceneCapture2D lifecycle, camera math, horror features |
| 137 | `ABP_PlanarCaptureActor` | `Public/Capture/BP_PlanarCaptureActor.h` | `Private/Capture/BP_PlanarCaptureActor.cpp` | Placeable surface actor with mesh, MDI, proximity trigger |
| 138 | `ASS_PlanarCaptureManager` | `Public/Capture/SS_PlanarCaptureManager.h` | `Private/Capture/SS_PlanarCaptureManager.cpp` | Global budget manager, RT pool, surface scoring |
### Capture Utility (Static Library)
| # | C++ Class | Header | Source | Purpose |
|---|-----------|--------|--------|---------|
| — | `UPlanarCaptureCameraUtils` | `Public/Capture/PlanarCaptureCameraUtils.h` | `Private/Capture/PlanarCaptureCameraUtils.cpp` | Mirror/portal/oblique projection math |
### Capture Common (Shared Types)
| # | C++ Class | Header | Source | Purpose |
|---|-----------|--------|--------|---------|
| — | `PlanarCaptureCommon.h` | `Public/Capture/PlanarCaptureCommon.h` | `Private/Capture/PlanarCaptureCommon.cpp` | Shared enums, structs, quality profiles |
---
*Status Checklist v2.0 — Updated 2026-05-21. See [`remaining-blueprint-build-order.md`](remaining-blueprint-build-order.md) for the prioritized build list.*
*Status Checklist v2.1 — Updated 2026-05-22. See [`remaining-blueprint-build-order.md`](remaining-blueprint-build-order.md) for the prioritized build list.*

View File

@@ -0,0 +1,197 @@
# Planar Capture System — Implementation Checklist
**Version:** 1.0 | **Phase:** 17 | **Systems:** 136-147 (12 systems)
Use this checklist to track implementation of the Planar Capture System. Each checkbox represents a discrete task.
---
## Phase 17a — C++ Core (Systems 136-138)
### C++ Files to Create
- [ ] `Source/PG_Framework/Public/Capture/PlanarCaptureCommon.h` — enums, structs, quality profiles
- [ ] `Source/PG_Framework/Private/Capture/PlanarCaptureCommon.cpp` — compilation stub
- [ ] `Source/PG_Framework/Public/Capture/PlanarCaptureCameraUtils.h` — static math library
- [ ] `Source/PG_Framework/Private/Capture/PlanarCaptureCameraUtils.cpp` — mirror/portal/oblique math
- [ ] `Source/PG_Framework/Public/Capture/BPC_PlanarCapture.h` — component header
- [ ] `Source/PG_Framework/Private/Capture/BPC_PlanarCapture.cpp` — component implementation
- [ ] `Source/PG_Framework/Public/Capture/BP_PlanarCaptureActor.h` — actor header
- [ ] `Source/PG_Framework/Private/Capture/BP_PlanarCaptureActor.cpp` — actor implementation
- [ ] `Source/PG_Framework/Public/Capture/SS_PlanarCaptureManager.h` — subsystem header
- [ ] `Source/PG_Framework/Private/Capture/SS_PlanarCaptureManager.cpp` — subsystem implementation
### Build Configuration
- [ ] Update `PG_Framework.Build.cs` — add `"Renderer"` and `"RenderCore"` to PublicDependencyModuleNames
- [ ] Update `PG_Framework.h` — include `Capture/PlanarCaptureCommon.h`
- [ ] Compile and fix any errors
- [ ] Verify module loads in UE5 editor
### Initial Testing
- [ ] Spawn `BP_PlanarCaptureActor` in test level (use Blueprint child with mesh)
- [ ] Verify `SS_PlanarCaptureManager` is auto-created by engine
- [ ] Verify `BPC_PlanarCapture` initializes without crash
- [ ] Verify `RegisterSurface` logs to output log
- [ ] Test `CaptureNow()` — verify no crash during capture
- [ ] Test `ShutdownCapture()` — verify RT returned to pool
---
## Phase 17b — Material Foundation (Systems 144, 145, 147)
### M_CaptureSurface_Master
- [ ] Create Material: Name=`M_CaptureSurface_Master`, Domain=Surface, ShadingModel=Unlit
- [ ] Build Layer 0: Texture Sample Parameter "CaptureTexture" with UV flip branch (bIsMirror static switch)
- [ ] Build Layer 1: Condensation normal (Panner + Normal Strength driven by DistortionAmplitude MPC)
- [ ] Build Layer 2: Fresnel edge fade (CameraVector · VertexNormal)
- [ ] Build Layer 3: Dirt/scratch multiply (DirtMask × DirtOpacity, tinted by SurfaceAge)
- [ ] Build Layer 4: Steam/fog lerp (animated noise × SteamIntensity)
- [ ] Build Layer 5: Steam emissive glow (noise × SteamEmissiveIntensity → Emissive Color)
- [ ] Build Layer 6: Text reveal mask (TextRevealMask × TextRevealProgress)
- [ ] Build Layer 7: Mirror darkness (multiply by 1.0 - MirrorDarkness)
- [ ] Build Layer 8: Wrong reflection crossfade (lerp NormalRT ↔ DelayedRT by WrongReflectionBlend)
- [ ] Add bIsMirror static switch (true = UV flip)
- [ ] Add bEnableHorrorLayers static switch (false = Layers 6-8 compiled out)
- [ ] Add 10 MPC Collection Parameter nodes referencing MPC_CaptureSurface
- [ ] Compile → zero material errors
### MPC_CaptureSurface
- [ ] Create Material Parameter Collection: `MPC_CaptureSurface`
- [ ] Add 10 scalar parameters: SteamIntensity(default 0), DirtOpacity(0), CondensationFlow(0), DistortionAmplitude(0), MirrorDarkness(0), WrongReflectionBlend(0), TextRevealProgress(0), SteamEmissiveIntensity(0), DelayedReflectionBlend(0), SurfaceAge(0)
- [ ] Add 2 vector parameters: SurfaceTintColor(1,1,1,1), SteamColor(0.8,0.85,0.9,1)
### Material Instances
- [ ] Create `MI_Mirror_Clean` — bIsMirror=true, bEnableHorrorLayers=false
- [ ] Create `MI_Mirror_Dirty` — DirtOpacity=0.4, SurfaceAge=0.3
- [ ] Create `MI_Mirror_Steam` — SteamIntensity=0.6, CondensationFlow=0.5
- [ ] Create `MI_Mirror_Horror` — bEnableHorrorLayers=true
- [ ] Create `MI_Portal_Standard` — bIsMirror=false
- [ ] Create `MI_Monitor_Security` — bIsMirror=false
- [ ] Create `MI_FakeWindow_Interior` — bIsMirror=false
---
## Phase 17c — Blueprint Actors (Systems 139-143)
### BP_Mirror (139)
- [ ] Create Blueprint: Parent=`BP_PlanarCaptureActor`, Name=`BP_Mirror`
- [ ] Set `CaptureComponent.CaptureMode = Mirror` in Class Defaults
- [ ] Assign plane mesh to `SurfaceMesh`
- [ ] Assign `MI_Mirror_Clean` to SurfaceMesh Material[0]
- [ ] Set `SurfaceMPC` = `MPC_CaptureSurface`
- [ ] Add custom events: `SetMirrorDirtLevel(Float)`, `SetMirrorSteamLevel(Float)`
- [ ] Test: place in level, walk up, verify reflection renders
### BP_Portal (140)
- [ ] Create Blueprint: Parent=`BP_PlanarCaptureActor`, Name=`BP_Portal`
- [ ] Add `TeleportTrigger` BoxComponent
- [ ] Set `CaptureComponent.CaptureMode = Portal`
- [ ] Assign `MI_Portal_Standard` to SurfaceMesh
- [ ] Implement `OnPortalOverlap` event with `HasAuthority` gate
- [ ] Implement `Server_TeleportThroughPortal` RPC
- [ ] Add `LinkedTargetSurface` variable (set per instance)
- [ ] Test: place source + target portals, verify view renders and teleport works
### BP_Monitor (141)
- [ ] Create Blueprint: Parent=`BP_PlanarCaptureActor`, Name=`BP_Monitor`
- [ ] Set `CaptureComponent.CaptureMode = Monitor, CaptureFOV = 70`
- [ ] Override QualityProfiles[0].CaptureInterval = 0.2 (5fps)
- [ ] Assign `MI_Monitor_Security` to SurfaceMesh
- [ ] Add `bPoweredOn`, `StaticNoiseIntensity`, `FlickerCurve` variables
- [ ] Implement `PowerOn()`, `PowerOff()`, `SetStaticNoise()` functions
- [ ] Place CameraActor in level, assign to `FixedCameraActor`
- [ ] Test: power on/off cycle, static noise, scanline appearance
### BP_HorrorMirror (142)
- [ ] Create Blueprint: Parent=`BP_Mirror`, Name=`BP_HorrorMirror`
- [ ] Set `CaptureComponent.CaptureMode = HorrorMirror`
- [ ] Hero tier: set `DelayedFrameCount = 5`
- [ ] Place `WrongReflectionActor` (Metahuman/ghost) hidden in level
- [ ] Create Float Curves: `Curve_WrongReflectionBlend`, `Curve_MirrorDarkness`, `Curve_UVDistortion`
- [ ] Build `HorrorScareTimeline` with 4 tracks
- [ ] Implement `TriggerHorrorScare()` event
- [ ] Wire to `BPC_ScareEventSystem.TriggerScareEvent()`
- [ ] Wire audio to `SS_AudioManager.PlaySoundAtLocation()`
- [ ] Test: trigger scare, verify wrong reflection appears and fades, MPC params return to zero
### BP_FakeWindow (143)
- [ ] Create Blueprint: Parent=`BP_PlanarCaptureActor`, Name=`BP_FakeWindow`
- [ ] Set `CaptureComponent.CaptureMode = FakeWindow, CaptureFOV = 100`
- [ ] Assign `MI_FakeWindow_Interior` to SurfaceMesh
- [ ] Create sublevel with view room + SceneCaptureCamera
- [ ] Implement `CheckPlayerDistance` timer for sublevel streaming
- [ ] Implement `SetWeatherOverlay(Weather)` function
- [ ] Test: sublevel streams in/out based on distance
---
## Phase 17d — Data Asset (System 146)
### DA_PlanarCaptureProfile
- [ ] Create Data Asset: `DA_PlanarCaptureProfile` (PrimaryDataAsset or custom C++ class)
- [ ] Add variables: DefaultMode, QualityProfileOverrides, ShowOnly/Hidden actors, bStartEnabled, bDestructible, material override
- [ ] Create profile instances: `DA_HeroMirror`, `DA_SecurityMonitor`, `DA_HorrorMirror_ScareReady`
- [ ] Add `CaptureProfile` variable to `BP_PlanarCaptureActor` (Instance Editable, Expose on Spawn)
- [ ] Wire BeginPlay to apply profile
---
## Phase 17e — Integration
### Scare System Integration
- [ ] `BP_HorrorMirror.TriggerHorrorScare()``BPC_ScareEventSystem.TriggerScareEvent(Tag)` (101)
- [ ] Create `DA_ScareEvent` instance for mirror apparition scare (references existing 127 pattern)
### Audio Integration
- [ ] All sound triggers route through `SS_AudioManager` (132)
- [ ] Mirror shatter: `SS_AudioManager.PlaySoundAtLocation()`
- [ ] Portal whoosh: `SS_AudioManager.PlaySoundAtLocation()`
- [ ] Monitor static/hum: `SS_AudioManager.PlaySoundAtLocation()`
### State Manager Integration
- [ ] Portal teleport: `BPC_StateManager.IsActionPermitted(Teleport Tag)` (130)
- [ ] Horror scare: `BPC_StateManager.IsActionPermitted(Scare Tag)` before triggering
- [ ] Add `Framework.Action.Teleport`, `Framework.Action.Scare` tags to GameplayTag data tables
### Persistence Integration
- [ ] `BP_PlanarCaptureActor` implements `I_Persistable` (36)
- [ ] `CollectState()`: save bIsDestroyed, DirtOpacity, SteamIntensity, SurfaceAge
- [ ] `RestoreState()`: apply saved state, update MPC params
### Input Integration
- [ ] Portal transition pushes inspection input context via `SS_EnhancedInputManager` (128)
### Camera Integration
- [ ] Portal transition applies FOV animation via `BPC_CameraStateLayer` (14) (optional)
---
## Phase 17f — Polish & Performance
- [ ] Performance test: 1 mirror @ Hero → GPU profiling baseline
- [ ] Performance test: 3 mirrors @ High → verify budget enforcement
- [ ] Performance test: 10 mirrors, walk around → verify quality tiers scale correctly
- [ ] Edge case: mirror behind wall → verify frustum cull sets Off
- [ ] Edge case: player very far → verify distance sets Off
- [ ] Edge case: mirror destroyed during scene capture → no crash
- [ ] Edge case: rapid enable/disable cycle → no RT leak
- [ ] Memory test: verify RT pool releases all targets on level unload
- [ ] Multiplayer test: server destroys mirror, verify clients disable
- [ ] Multiplayer test: two players look at same mirror, verify both see correct reflection
---
## Documentation Verification
- [ ] `docs/blueprints/17-capture/` — 12 spec files created and reviewed
- [ ] `docs/blueprints/INDEX.md` — updated with 17-capture section
- [ ] `docs/developer/INDEX.md` — updated with 17-capture-systems entry
- [ ] `docs/developer/17-capture-systems.md` — developer reference created
- [ ] `CONTEXT.md` — Phase 17 added to Build Order, directory structure updated
- [ ] `UE5_Modular_Game_Framework.md` — systems 136-147 documented
- [ ] `docs/architecture/planar-capture-system.md` — architecture spec created
- [ ] `docs/architecture/blueprint-limitations-workarounds.md` — SceneCapture2D C++ workarounds added
- [ ] `docs/checklists/cpp-blueprint-status.md` — 12 new systems added to status grid
---
*Planar Capture System Checklist v1.0 — Track each checkbox as you implement.*

View File

@@ -773,6 +773,11 @@ STEP 22 — Adaptive BPs (89-101)
STEP 23 — Meta + Settings + Polish (102-114)
STEP 24 — SS_AudioManager + BP_RoomAudioZone (132-133)
STEP 25 — All DA_* Data Asset instances (content-populated, per project)
STEP 26 — Phase 17: Planar Capture C++ (136-138) — UBPC_PlanarCapture, ABP_PlanarCaptureActor, ASS_PlanarCaptureManager
STEP 27 — Phase 17b: Materials (144, 145, 147) — M_CaptureSurface_Master, MPC_CaptureSurface, 7 MI_ instances
STEP 28 — Phase 17c: Blueprint Actors (139-143) — BP_Mirror, BP_Portal, BP_Monitor, BP_HorrorMirror, BP_FakeWindow
STEP 29 — Phase 17d: Data Asset (146) — DA_PlanarCaptureProfile instances
STEP 30 — Phase 17e: Integration — wire to BPC_ScareEventSystem (101), SS_AudioManager (132), BPC_StateManager (130)
```
---
@@ -789,6 +794,7 @@ Every system has a complete spec in `docs/blueprints/` with node-by-node Manual
| `docs/architecture/sound-catalog.md` | 150+ sound triggers, 14 surface table entries |
| `docs/architecture/multiplayer-networking.md` | HasAuthority() gates, Server_ RPCs, RepNotify patterns |
| `docs/architecture/blueprint-limitations-workarounds.md` | C++-only functions you'll encounter during BP implementation |
| `docs/architecture/planar-capture-system.md` | Mirror/portal/monitor/horror capture system architecture (12 systems, Phase 17) |
| `docs/developer/cpp-integration-guide.md` | Per-C++-class setup steps, usage patterns, BP child requirements |
---
@@ -797,13 +803,17 @@ Every system has a complete spec in `docs/blueprints/` with node-by-node Manual
| Asset Type | Total | C++-Covered | BP to Build |
|-----------|-------|------------|-------------|
| **BP Components (BPC_)** | 79 | 4 full C++, 8 stubs | 67 BP children (8 have C++ stubs to extend) |
| **BP Actors (BP_)** | 11 | 0 | 11 BP actor children |
| **BP Components (BPC_)** | 80 | 5 full C++, 8 stubs | 67 BP children (8 have C++ stubs to extend) |
| **BP Actors (BP_)** | 16 | 1 full C++ | 15 BP actor children |
| **Widget BPs (WBP_)** | 14 | 0 | 14 Widget BPs |
| **Data Assets (DA_)** | 18 | 3 full C++, 2 stubs | ~200+ DA instances (content-driven) |
| **Data Assets (DA_)** | 19 | 3 full C++, 2 stubs | ~200+ DA instances (content-driven) |
| **GameInstance Subsystems (SS_)** | 7 | 3 full C++ | 4 BP children |
| **World Subsystems (SS_)** | 1 | 1 full C++ | 0 |
| **Materials (M_)** | 1 | 0 | 1 Material |
| **Material Instances (MI_)** | 7 | 0 | 7 MICs |
| **MPC** | 1 | 0 | 1 MPC |
| **Interfaces (I_)** | 10 | 9 in C++ | 1 (I_HidingSpot) |
| **Function Libraries (FL_)** | 1 | 1 in C++ | 0 |
| **Function Libraries (FL_)** | 2 | 2 in C++ | 0 |
| **GameInstance (GI_)** | 1 | 1 in C++ | 1 BP child |
| **GameMode (GM_)** | 1 | 1 in C++ | 1 BP child |
| **GameState (GS_)** | 1 | 1 in C++ | 1 BP child |

View File

@@ -0,0 +1,281 @@
# Developer Reference — Capture Systems (Systems 136-147)
**Version:** 1.0 | **Systems:** 136-147 (12 systems) | **Phase:** 17 | **Layer:** Rendering & Visual
---
## Architecture Overview
The Planar Capture System provides a unified rendering pipeline for mirrors, portals, monitors, and horror surfaces. All performance-critical work (camera math, render target management, scene capture lifecycle, quality budget enforcement) runs in C++, while designer-facing configuration, event scripting, and material authoring live in Blueprint.
### System Layers
```
┌──────────────────────────────────────────────────────────────────────┐
│ BLUEPRINT CONTENT LAYER │
│ BP_Mirror → BP_HorrorMirror Designer places in level, configures │
│ BP_Portal → BP_Monitor → BP_FakeWindow │
│ DA_PlanarCaptureProfile → MPC_CaptureSurface → MI_* material instances│
├──────────────────────────────────────────────────────────────────────┤
│ C++ RUNTIME LAYER │
│ ASS_PlanarCaptureManager ← Global budget, RT pool, scoring │
│ ABP_PlanarCaptureActor ← Surface mesh, MDI, registration │
│ UBPC_PlanarCapture ← SceneCapture2D, camera math, horror │
│ UPlanarCaptureCameraUtils ← Static math: mirror, portal, oblique │
├──────────────────────────────────────────────────────────────────────┤
│ UE5 ENGINE LAYER │
│ USceneCaptureComponent2D UTextureRenderTarget2D │
│ UMaterialParameterCollectionInstance UWorldSubsystem │
└──────────────────────────────────────────────────────────────────────┘
```
---
## Data Flow: Mirror Reflection
### Step-by-Step: Player Looks at a Mirror
1. **Registry:** `BP_Mirror.BeginPlay()` calls `SS_PlanarCaptureManager.RegisterSurface(this)` — the mirror is now tracked globally.
2. **Evaluation (0.5s interval):** `SS_PlanarCaptureManager.Tick()` calls `EvaluateAllSurfaces()`.
3. **Scoring:** For each registered surface, `BPC_PlanarCapture.GetCurrentScore()` computes:
- Screen coverage (how much screen space does the surface occupy?)
- Facing angle (is the player looking at the surface or edge-on?)
- Distance to viewer
- Scripted priority override
- Visibility frustum check
4. **Tier Assignment:** Based on composite score and budget limits, the manager calls `BPC_PlanarCapture.ApplyQualityTier(Tier)`.
5. **Capture Tick (per frame):** `BPC_PlanarCapture.TickComponent()` checks time since last capture vs the quality tier's `CaptureInterval`. If it's time to capture:
- Gets the player camera transform
- Calls `ComputeCaptureCameraTransform()` — for Mirror mode, this calls `UPlanarCaptureCameraUtils::ComputeMirroredTransform()` which reflects the player camera across the mirror plane
- Sets the `USceneCaptureComponent2D` world transform
- Calls `SceneCapture->CaptureScene()` — renders to the allocated `UTextureRenderTarget2D`
- Calls `PushMPCParameters()` to update the MPC with current horror/visual params
6. **Material Display:** `M_CaptureSurface_Master` samples the render target texture, applies UV flip (for mirror), and layers any active effects (dirt, steam, condensation). The output is displayed on the surface mesh.
### Quality Tier Transitions
```
Off ←──→ Low ←──→ Medium ←──→ High ←──→ Hero
(score=0) (>0) (≥0.2) (≥0.5) (≥0.8)
Demotion happens when:
- Score drops below threshold (player walks away)
- Budget limit exceeded (too many Hero surfaces)
- GlobalQualityCap clamps tier
- Force tier override is released
Promotion happens when:
- Score rises above threshold (player approaches)
- Budget slots open up (higher-tier surface destroyed or goes Off)
- Scripted priority boost kicks in
```
---
## State Machine: Quality Tier Lifecycle
```
[Surface Spawns]
┌─────────┐ score > 0 ┌─────────┐
│ OFF │ ────────────────► │ LOW │
└─────────┘ └────┬────┘
▲ │ score ≥ 0.2
│ score = 0 ┌────▼────┐
│ or !inFrustum │ MEDIUM │
│ └────┬────┘
│ │ score ≥ 0.5
│ ┌────▼────┐
│ │ HIGH │
│ └────┬────┘
│ │ score ≥ 0.8
│ ┌────▼────┐
│ │ HERO │
│ └─────────┘
┌────┴────────┐
│ SURFACE │ Called on EndPlay, DisableSurface, or DestroySurface
│ DESTROYED │
└─────────────┘
```
---
## Budget Enforcement
The `SS_PlanarCaptureManager` enforces three levels of budget:
1. **Per-Tier Count Limits:** `MaxHeroSurfaces` (default 1), `MaxHighSurfaces` (3), `MaxMediumSurfaces` (6). If 4 surfaces score ≥ 0.5, only the top 3 get High tier — the 4th gets Medium even if it scored for High.
2. **Total Memory Budget:** `MaxTotalRenderTargetMemoryMB` (default 128MB). If exceeded, the manager logs a warning. Future: automatic demotion of lowest-priority surfaces until under budget.
3. **Global Quality Cap:** `GlobalQualityCap` (default High). No surface exceeds this tier regardless of score. Set to Medium on Steam Deck / lower hardware.
---
## Render Target Pool
The pool reduces memory allocation overhead by reusing render targets:
```
RequestRenderTarget(Size):
1. Search pool for entry with matching Size AND !bInUse
2. If found → mark bInUse=true, return RT
3. If not → CreateRenderTarget(Size) → add to pool
ReleaseRenderTarget(RT):
1. Find pool entry for this RT
2. Mark bInUse=false (RT stays allocated, ready for next request)
```
This means the pool stabilizes after initial allocation — no new RTs are created unless a new size is requested.
---
## Horror Features Deep Dive
### Wrong Reflection (ActivateHorrorReflection)
```
1. UBPC_PlanarCapture saves current ShowOnlyActors → SavedShowOnlyActors
2. ShowOnlyActors is cleared
3. WrongReflectionActor is added to ShowOnlyActors
4. UpdateActorLists() pushes to USceneCaptureComponent2D::ShowOnlyActors
5. Next capture frame: SceneCapture2D now ONLY renders the WrongReflectionActor
6. Material: WrongReflectionBlend MPC param crossfades between normal RT and wrong reflection
```
### Delayed Frame Ring Buffer
```
Hero tier with DelayedFrameCount = 5:
1. FrameRingBuffer array has 5 render targets
2. Each capture frame: RingBufferWriteIndex = (index + 1) % 5
3. Material: DelayedReflectionBlend MPC param blends between current frame and oldest ring buffer frame
4. Effect: Player's reflection lags 5 frames behind — unsettling
```
### Steam Text Reveal
```
TriggerHorrorScare() → Timeline → TextRevealProgress MPC param ramps 0→1
Material Layer 6: TextRevealMask texture is wiped from 0 to 1
Appearance: "HELP ME" appears to write itself in the steam on the mirror
```
---
## Integration Points
### BPC_StateManager (130)
```cpp
// Portal teleport must be gated
if (!StateManager->IsActionPermitted(FGameplayTag::RequestGameplayTag("Framework.Action.Teleport")))
return; // Blocked — player is dead, in cutscene, etc.
```
### BPC_ScareEventSystem (101)
```cpp
// Horror mirror triggers coordinated scare
ScareEventSystem->TriggerScareEvent(ScareEventTag);
// This coordinates lights (96), audio (132), pacing (98), stress (10)
```
### SS_AudioManager (132)
```cpp
// All surface audio routes through audio subsystem
AudioManager->PlaySoundAtLocation(MirrorShatterCue, GetActorLocation());
AudioManager->PlaySoundAtLocation(PortalWhooshCue, GetActorLocation());
```
### I_Persistable (36)
```cpp
// Surface state persists across saves
// BP_PlanarCaptureActor implements I_Persistable:
CollectState() { bIsDestroyed, CurrentDirtLevel, CurrentSteamLevel }
RestoreState() { Apply saved state }
```
### SS_EnhancedInputManager (128)
```cpp
// Portal transition switches input context
InputManager->PushContext(PortalTransitionContext, EInputContextPriority::Inspection);
// Player exits portal → PopContext
```
---
## Multiplayer Networking
### What Replicates
- **`bRepIsActive`** on `BP_PlanarCaptureActor` — server tells all clients whether a surface is active (e.g., a mirror was shattered by another player)
### What Does NOT Replicate (Local-Only)
- **All capture rendering** — each client renders their own view with their own camera perspective. There is zero reason to replicate render targets.
- **Quality tiers** — each client evaluates surfaces independently (different camera positions mean different scores)
- **Horror effects** — triggered server-side (scare event), executed locally on each client
### Server-Authoritative Pattern
```
Server: BP_Mirror.DestroySurface() [HasAuthority]
→ Set bRepIsActive = false
→ OnRep_IsActive fires on all clients
→ Each client independently: DisableSurface() → ShutdownCapture()
```
---
## Performance Characteristics
| Tier | GPU Cost (relative) | VRAM per Surface | CPU Cost |
|------|---------------------|------------------|----------|
| Hero | 16x baseline | 16 MB | High (60 captures/sec) |
| High | 4x baseline | 4 MB | Medium (30/sec) |
| Medium | 1x baseline | 1 MB | Low (15/sec) |
| Low | 0.25x baseline | 256 KB | Minimal (4/sec) |
| Off | 0 | 0 | Zero |
### Optimization Tips
1. **MaxCaptureDistance:** Set lower (3000-5000) for indoor levels — mirrors far away go Off
2. **FullEvaluationInterval:** Increase to 1.0s for large levels with many mirrors to reduce CPU scoring cost
3. **GlobalQualityCap:** Set to Medium on Steam Deck. Set to High on consoles.
4. **ShowOnly lists:** Use aggressively. Capturing only 5 actors is vastly cheaper than capturing 500.
5. **Monitor FPS:** Monitors should use 5fps Low tier — they don't need real-time updates.
6. **Lumen on capture:** Only enable on Hero tier. Each Lumen-enabled capture is ~3x more expensive.
---
## Debugging
### Console Commands
```
// Show all registered capture surfaces
SS_PlanarCaptureManager.GetSurfaceCount()
// Show pool memory usage
SS_PlanarCaptureManager.GetPoolMemoryUsageMB()
// Force all to max quality (for visual debugging)
SS_PlanarCaptureManager.ForceAllSurfacesToTier(Hero)
SS_PlanarCaptureManager.ReleaseForceTier()
// Show capture FPS (add to BPC_PlanarCapture debug mode)
stat SceneRendering
```
### Visual Debug
- **Unlit view mode:** See raw render target output without material effects
- **Wireframe:** Verify camera frustum on SceneCaptureComponent2D
- **Stat GPU:** SceneCapture passes appear under "SceneCapture" category
---
## Build Order (Phase 17)
| Sub-Phase | Systems | Dependencies |
|-----------|---------|-------------|
| 17a — C++ Core | 136, 137, 138, `PlanarCaptureCommon`, `PlanarCaptureCameraUtils` | Renderer module |
| 17b — Materials | 144, 145, 147 | 17a (for MPC parameter names) |
| 17c — Blueprint Actors | 139, 140, 141, 142, 143 | 17a + 17b |
| 17d — Data Assets | 146 | 17a |
| 17e — Integration | Wire to 101, 132, 130, 128, 36 | 17c |
---
*Developer Reference — Capture Systems v1.0. Companion to Blueprint Spec files in `docs/blueprints/17-capture/`.*

View File

@@ -1,6 +1,6 @@
# Developer Reference — UE5 Modular Game Framework
**Version:** 1.5 | **Generated:** 2026-05-21 | **Files:** 18 (1 index + 2 overview + 1 migration + 1 integration + 1 prototype + 1 starter + 10 category docs + 1 combined) | **C++:** 12 full + 10 stubs = 22 systems
**Version:** 1.6 | **Generated:** 2026-05-22 | **Files:** 19 (1 index + 2 overview + 1 migration + 1 integration + 1 prototype + 1 starter + 10 category docs + 1 combined + 1 capture) | **C++:** 15 full + 10 stubs = 25 systems
This directory contains developer-facing reference documentation for every system in the framework. Unlike the blueprint spec files (which define *what* to build), these documents explain *how each system works internally* — the data flow, state machines, integration points, and design rationale. Use these when you need to understand a system's behavior to implement, debug, or extend it.
@@ -32,7 +32,8 @@ docs/developer/
├── 08-weapons-systems.md ← Weapons, equipment & damage (systems 69-79)
├── 09-ai-systems.md ← AI, perception & encounters (systems 80-88)
├── 10-adaptive-systems.md ← Adaptive environment & atmosphere (systems 89-101, 132-133)
── 11-16-systems.md ← Meta, Settings, Polish, Data Assets, Input, State (systems 102-135)
── 11-16-systems.md ← Meta, Settings, Polish, Data Assets, Input, State (systems 102-135)
└── 17-capture-systems.md ← Planar Capture System — Mirrors, Portals, Monitors, Horror (systems 136-147)
```
## Quick Reference — Every System at a Glance

View File

@@ -199,8 +199,21 @@ Content/Game/ ← ALL game content (NEVER modify Fr
│ ├── BP_LightEvent_Flicker.uasset
│ ├── BP_ScareEvent_LockerBang.uasset
│ ├── BP_ScareEvent_Mirror.uasset
│ ├── BP_HorrorMirror_Morgue.uasset ← Horror mirror (wrong reflection scare)
│ ├── BP_Mirror_NurseStation.uasset ← Standard mirror (checkpoint reflection)
│ └── BP_AtmosphereController_WardA.uasset
├── Capture/ ← Planar Capture surface actors
│ ├── BP_Mirror_NurseStation.uasset ← Standard mirror: Mode=Mirror, aged, checkpoint
│ ├── BP_HorrorMirror_Morgue.uasset ← Horror mirror: wrong reflection, steam scare
│ ├── BP_HorrorMirror_WardensOffice.uasset ← Horror mirror: final entity confrontation
│ ├── BP_Monitor_Security.uasset ← Security monitor: hallway camera feed, scanlines
│ ├── BP_Monitor_Morgue.uasset ← Morgue monitor: static noise, flicker, Shade cameo
│ ├── BP_Portal_VoidEntrance.uasset ← Void portal entrance (Basement → VoidSpace)
│ ├── BP_Portal_VoidExit.uasset ← Void portal exit (VoidSpace → Basement)
│ ├── BP_FakeWindow_WardensOffice.uasset ← Atmospheric window: rain, night, parallax
│ └── DA_PlanarCaptureProfile_Mirror.uasset← Capture profile Data Assets
├── Audio/ ← Game-specific audio assets
│ ├── DA_AudioSettings_Horror.uasset ← Audio bus config
│ ├── BP_RoomAudioZone_WardA.uasset ← Room audio zone volumes
@@ -600,6 +613,22 @@ Each game asset proves a specific framework system works. Below: every framework
| 130 | BPC_StateManager | Hiding blocks fire; death blocks interaction; cutscene blocks all |
| 131 | DA_StateGatingTable | Designer-configurable state permission rules |
### 17-capture (Planar Capture — 12 systems)
| # | Framework System | Demonstrated By |
|---|-----------------|----------------|
| 136 | BPC_PlanarCapture | All capture surfaces (component managing SceneCapture2D lifecycle) |
| 137 | BP_PlanarCaptureActor | BP_Mirror, BP_Portal, BP_Monitor, BP_HorrorMirror, BP_FakeWindow (all extend this) |
| 138 | SS_PlanarCaptureManager | Auto-created WorldSubsystem — manages quality budget across all 8 surfaces |
| 139 | BP_Mirror | BP_Mirror_NurseStation — standard reflection mirror with aging |
| 140 | BP_Portal | BP_Portal_VoidEntrance/Exit — void portal teleport with clip plane |
| 141 | BP_Monitor | BP_Monitor_Security/Morgue — security camera feeds, scanlines, static noise |
| 142 | BP_HorrorMirror | BP_HorrorMirror_Morgue/WardensOffice — wrong reflection, steam, text, scare events |
| 143 | BP_FakeWindow | BP_FakeWindow_WardensOffice — atmospheric window with weather overlay |
| 144 | M_CaptureSurface_Master | All mirror/portal/monitor surfaces (9-layer material stack) |
| 145 | MPC_CaptureSurface | All 10 MPC scalar params (SteamIntensity, DirtOpacity, MirrorDarkness, etc.) |
| 146 | DA_PlanarCaptureProfile | DA_PlanarCaptureProfile_Mirror — per-surface quality config |
| 147 | MI_MirrorInstances | MI_Mirror_Clean/Dirty/Steam/Horror, MI_Portal_Standard, MI_Monitor_Security, MI_FakeWindow_Interior |
---
## Build Order (Dependency-Safe)
@@ -625,7 +654,8 @@ Follow this order when creating uasset files in UE5:
| 14 | **Data Assets (Content)** — Encounters, Atmosphere, Scares, Puzzles, Objectives, Behaviour, Acoustic | DA_* parents | 20+ |
| 15 | **Checkpoints** — BP_Checkpoint_SafeRoom, _Mirror | BP_Checkpoint | 2 |
| 16 | **Room Audio Zones** — BP_RoomAudioZone_* | BP_RoomAudioZone | 3 |
| 17 | **Encounter Spawners** — BP_EncounterSpawner_* | BPC_ProceduralEncounter | 3 |
| 17 | **Capture Surfaces** — BP_Mirror_NurseStation, BP_HorrorMirror_Morgue/WardensOffice, BP_Monitor_Security/Morgue, BP_Portal_VoidEntrance/Exit, BP_FakeWindow_WardensOffice | BP_PlanarCaptureActor | 9 |
| 18 | **Encounter Spawners** — BP_EncounterSpawner_* | BPC_ProceduralEncounter | 3 |
| 18 | **Atmosphere Actors** — BP_LightEvent_*, BP_ScareEvent_*, BP_AtmosphereController_* | BPC_* parent | 6 |
| 19 | **Tutorial Triggers** — BP_TutorialTrigger_* | BPC_TutorialSystem | 3 |
| 20 | **Polish** — BP_CreditsSequence, dev cheats, debug menu | Framework | 3 |
@@ -657,6 +687,7 @@ Each `docs/game/` file explains how to build a specific section of the prototype
| [`enemies-index.md`](enemies-index.md) | All enemies — Patient, Shade, Orderly — AI controllers, BT, perception | 12 |
| [`narrative-progression.md`](narrative-progression.md) | Objectives, dialogue, cutscenes, ending accumulator, branching | 13 |
| [`atmosphere-audio.md`](atmosphere-audio.md) | Atmosphere profiles, scares, lighting, room audio zones, adaptive environment | 14, 16, 17, 18 |
| [`planar-capture-examples.md`](planar-capture-examples.md) | Mirrors, horror mirrors, security monitors, void portals, fake windows — all 8 capture surfaces | 17 |
| [`save-checkpoints.md`](save-checkpoints.md) | Save system, checkpoints, death loop, void space, persistence | 15 |
| [`polish-loading-credits.md`](polish-loading-credits.md) | Tutorials, loading screen, credits, debug, analytics | 19, 20 |
| [`state-gating-examples.md`](state-gating-examples.md) | DA_StateGatingTable game-specific rules | 14 |

View File

@@ -0,0 +1,409 @@
# Planar Capture System Examples — Project Void Horror Game
**Game:** Project Void | **Build Phase:** 17e (Capture System Integration)
**Framework Systems:** 136-147 (12 Planar Capture systems)
---
## Purpose
Defines how the Planar Capture System is used in the Project Void horror game — mirrors that show wrong reflections, security monitors that display camera feeds, and void portals that warp reality. Every capture surface mode is demonstrated.
---
## Capture Surface Placement Map
| Surface | Level | Location | Mode | Purpose |
|---------|-------|----------|------|---------|
| `BP_Mirror_NurseStation` | WardA | Nurses Station | Mirror | Checkpoint mirror, also shows player reflection |
| `BP_HorrorMirror_Morgue` | WardB | Morgue | HorrorMirror | Wrong reflection scare (face appears behind player) |
| `BP_HorrorMirror_WardensOffice` | WardensOffice | Above desk | HorrorMirror | Final confrontation — entity speaks through mirror |
| `BP_Monitor_Security` | WardA | Security Office | Monitor | Shows hallway camera feed (live) |
| `BP_Monitor_Morgue` | WardB | Morgue office | Monitor | Shows morgue interior (static noise, flicker) |
| `BP_Portal_VoidEntrance` | Basement | Crematorium | Portal | Void portal — links to BP_Portal_VoidExit in VoidSpace |
| `BP_Portal_VoidExit` | VoidSpace | Memory Shrine 3 | Portal | Exit portal back to Basement |
| `BP_FakeWindow_WardensOffice` | WardensOffice | Window behind desk | FakeWindow | Shows "outside" view (rain, night, distant asylum) |
---
## 1. BP_Mirror_NurseStation — Standard Mirror
**Asset Path:** `Content/Game/Actors/BP_Mirror_NurseStation.uasset`
**Parent:** `BP_Mirror` (139)
**Framework Systems:** 136-139, 144-147
### Level Placement
```
L_Asylum_WardA → Nurses Station
├─ BP_Mirror_NurseStation (640×320 plane, wall-mounted)
├─ Position: Above sink counter, eye-level
├─ Material: MI_Mirror_Dirty (aged, slight oxidation)
├─ MPC params: DirtOpacity=0.3, SurfaceAge=0.2
└─ ProximityTrigger: 1000uu radius
```
### Gameplay Integration
- **Checkpoint:** This mirror doubles as `BP_Checkpoint_Mirror` — player touches it to save
- **Reflection quality:** Player is in a small room → mirror stays at High/Medium tier (close proximity)
- **No horror features** — this is a normal mirror at this point in the game
- **Easter egg:** Player's reflection blinks slightly out of sync (1 in 100 chance, very subtle)
### Blueprint Setup
```
BP_Mirror_NurseStation (child of BP_Mirror)
├─ Class Defaults:
│ ├─ CaptureComponent.CaptureMode = Mirror
│ ├─ CaptureComponent.QualityProfiles[1] = Medium (512px, 15fps, shadows)
│ ├─ CaptureComponent.QualityProfiles[2] = High (1024px, 30fps, Lumen)
│ ├─ SurfaceMesh → SM_WallMirror_640x320
│ └─ SurfaceMaterial → MI_Mirror_Dirty
└─ Event BeginPlay:
├─ SetSurfaceMPCParameter("DirtOpacity", 0.3)
├─ SetSurfaceMPCParameter("SurfaceAge", 0.2)
└─ EnableSurface()
```
---
## 2. BP_HorrorMirror_Morgue — Wrong Reflection Scare
**Asset Path:** `Content/Game/Actors/BP_HorrorMirror_Morgue.uasset`
**Parent:** `BP_HorrorMirror` (142)
**Framework Systems:** 136-138, 142, 144-147 + 101_BPC_ScareEventSystem
### Level Placement
```
L_Asylum_WardB → Morgue
├─ BP_HorrorMirror_Morgue (800×600 plane)
├─ Position: On morgue wall, facing dissection table
├─ Material: MI_Mirror_Horror (horror layers enabled)
├─ WrongReflectionActor: BP_Enemy_Shade placed behind camera (hidden)
└─ Trigger: Player looks at mirror > 2 seconds
```
### Scare Sequence
```
Trigger: BPC_InteractionDetector focus remains on mirror for 2+ seconds
├─ [Anticipation Phase] (5 seconds)
│ ├─ Steam slowly fogs mirror:
│ │ └─ Timeline: SteamIntensity MPC 0.0 → 0.4 over 3s
│ ├─ Audio: SS_AudioManager.PlaySFX("mirror_steam_hiss")
│ ├─ BPC_StressSystem.AddStress(+3) — subtle unease
│ └─ Player is still watching their reflection
├─ [Payoff Phase] (3 seconds)
│ ├─ BPC_ScareEventSystem.TriggerScareEvent("MirrorApparition")
│ │ └─ Coordinates with lights (flicker), audio (stinger)
│ │
│ ├─ CaptureComponent.ActivateHorrorReflection()
│ │ └─ Swaps ShowOnly list from Self to WrongReflectionActor
│ │ └─ Next capture frame: Shade appears in mirror instead of player
│ │
│ ├─ Timeline: WrongReflectionBlend MPC 0.0 → 1.0 over 0.5s (instant crossfade)
│ ├─ Timeline: MirrorDarkness MPC 0.0 → 0.6 (mirror darkens)
│ ├─ Timeline: DistortionAmplitude MPC 0.0 → 0.3 (mirror "breathes")
│ │
│ ├─ Steam text reveal: TextRevealProgress MPC 0.0 → 1.0
│ │ └─ Text "JOIN ME" appears in steam on mirror surface
│ │
│ └─ Audio: SS_AudioManager.PlaySFX("mirror_scare_stinger")
│ └─ Loud glass crack + whisper voice
└─ [Recovery Phase] (3 seconds)
├─ CaptureComponent.DeactivateHorrorReflection()
│ └─ Restores player's reflection
├─ All MPC params return to 0.0 over 1.5s
├─ BPC_StressSystem.AddStress(+15) — spike
└─ Mirror returns to normal (player can continue)
```
### Blueprint Setup
```
BP_HorrorMirror_Morgue (child of BP_HorrorMirror)
├─ Class Defaults:
│ ├─ CaptureComponent.CaptureMode = HorrorMirror
│ ├─ CaptureComponent.WrongReflectionActor = BP_Enemy_Shade_Reflection
│ │ └─ (This Shade instance is placed behind a wall — invisible to player
│ │ but visible to the capture camera due to ShowOnly list)
│ ├─ CaptureComponent.QualityProfiles[2].DelayedFrameCount = 3
│ │ └─ Reflection lags 3 frames at High tier
│ ├─ ScareEventTag = "Game.Environment.Scare.MirrorApparition"
│ └─ ScareDuration = 8.0
├─ Curves:
│ ├─ Curve_WrongReflectionBlend: (0,0) → (0.5,1.0) → (2.5,1.0) → (3.0,0)
│ ├─ Curve_MirrorDarkness: (0,0) → (0.5,0.6) → (2.0,0.4) → (3.0,0)
│ ├─ Curve_UVDistortion: (0,0) → (0.5,0.3) → (1.5,0.15) → (3.0,0)
│ └─ Curve_SteamTextReveal: (1.0,0) → (2.0,1.0) → (3.0,1.0)
└─ Event Graph:
├─ Bind to BPC_InteractionDetector.OnFocusGained
│ └─ If FocusedActor == Self → Start LookTimer (2.0s)
│ └─ On Timer Complete → TriggerHorrorScare()
└─ TriggerHorrorScare() (implemented as described above)
```
---
## 3. BP_Monitor_Security — Security Camera Display
**Asset Path:** `Content/Game/Actors/BP_Monitor_Security.uasset`
**Parent:** `BP_Monitor` (141)
**Framework Systems:** 136-138, 141, 144-147
### Level Placement
```
L_Asylum_WardA → Security Office
├─ BP_Monitor_Security (screen quad on desk)
├─ FixedCameraActor: SecurityCamera_HallwayA (placed in hallway)
├─ Shows live feed of hallway → player can see enemies patrolling
└─ Power: ON by default
```
### Monitor Behavior
```
BP_Monitor_Security
├─ Class Defaults:
│ ├─ CaptureComponent.CaptureMode = Monitor
│ ├─ CaptureComponent.CaptureFOV = 70
│ ├─ CaptureComponent.QualityProfiles[0].CaptureInterval = 0.2 (5fps)
│ │ └─ Security monitors don't need high FPS
│ ├─ CaptureComponent.FixedCameraActor = SecurityCamera_HallwayA
│ └─ SurfaceMaterial → MI_Monitor_Security
├─ Features:
│ ├─ Static noise overlay: StaticNoiseIntensity = 0.1 (subtle grain)
│ ├─ Scanline effect: bShowScanlines = true (CRT-like lines)
│ └─ Power state reacts to fusebox puzzle:
│ ├─ Fusebox power OFF → Monitor flickers and dies (PowerOff)
│ └─ Fusebox power ON → Monitor boots up with flicker (PowerOn)
└─ Narrative hook:
└─ At specific narrative flag ("ShadeEncountered"):
└─ Shade briefly appears on monitor for 1 frame (rare event)
└─ BPC_RareEventSystem check every 30s, 2% chance
```
### Blueprint Setup
```
BP_Monitor_Security (child of BP_Monitor)
├─ Class Defaults:
│ ├─ CaptureComponent.CaptureMode = Monitor
│ ├─ CaptureComponent.CaptureFOV = 70
│ ├─ bPoweredOn = true
│ ├─ bShowScanlines = true
│ ├─ StaticNoiseIntensity = 0.1
│ └─ SurfaceMaterial = MI_Monitor_Security
└─ Event Graph:
├─ Event BeginPlay → EnableSurface
├─ [Bind to fusebox puzzle]
│ ├─ Listen for BPC_UsableWorldObjectSystem.OnActivated (fusebox power)
│ │ └─ PowerOn() (with 1s flicker delay)
│ └─ Listen for BPC_UsableWorldObjectSystem.OnDeactivated
│ └─ PowerOff() (immediate)
└─ [Rare event: Shade on monitor]
└─ Timer (30s loop) → Check narrative flag "ShadeEncountered"
└─ If true → Random Float 0-1 < 0.02
└─ True: Show Shade frame (capture swap to Shade actor for 0.2s)
```
---
## 4. BP_Portal_VoidEntrance / BP_Portal_VoidExit — Void Portal
**Asset Path:** `Content/Game/Actors/BP_Portal_VoidEntrance.uasset`
**Parent:** `BP_Portal` (140)
**Framework Systems:** 136-138, 140, 144-147 + 130_BPC_StateManager
### Level Placement
```
L_Asylum_Basement → Crematorium
├─ BP_Portal_VoidEntrance (wall-mounted portal frame)
├─ LinkedTargetSurface: BP_Portal_VoidExit in L_Asylum_VoidSpace
├─ TeleportOnOverlap: true
├─ Appearance: Shimmering purple-black void surface
└─ Trigger: Player walks into it
```
```
L_Asylum_VoidSpace → Exit Chamber (behind Memory Shrine 3)
├─ BP_Portal_VoidExit (floating portal in void)
├─ LinkedTargetSurface: BP_Portal_VoidEntrance (back to Basement)
├─ Shows: Basement Crematorium view (player's return destination)
└─ Only visible after all 3 Memory Shrines activated
```
### Portal Teleport Flow
```
Player overlaps BP_Portal_VoidEntrance.TeleportTrigger
├─ [Server-authoritative check]
│ └─ Switch HasAuthority
│ ├─ Authority:
│ │ ├─ BPC_StateManager.IsActionPermitted(Framework.Action.Teleport)?
│ │ │ └─ True → proceed
│ │ │ └─ False → Show blocked feedback, return
│ │ │
│ │ ├─ Get LinkedTargetSurface → Get ActorTransform
│ │ ├─ Teleport player: SetActorLocation + SetActorRotation
│ │ │ └─ Offset: targetForward * 300 (spawn 3m in front of exit)
│ │ │
│ │ ├─ [Transition Effects]
│ │ │ ├─ WBP_ScreenEffectController.Flash(purple, 0.5s)
│ │ │ ├─ BPC_CameraStateLayer.PushLayer("PortalTransition", 1.0s)
│ │ │ │ └─ FOV 120 → 90 over 1s, chromatic aberration
│ │ │ └─ SS_AudioManager.PlaySFX("portal_travel")
│ │ │
│ │ └─ Multicast_OnPlayerTeleported (visual feedback for all clients)
│ │
│ └─ Remote (client):
│ └─ Call Server_TeleportThroughPortal RPC
└─
```
### Blueprint Setup
```
BP_Portal_VoidEntrance (child of BP_Portal)
├─ Class Defaults:
│ ├─ CaptureComponent.CaptureMode = Portal
│ ├─ bTeleportOnOverlap = true
│ ├─ bOneWay = false (two-way — player can return)
│ ├─ SurfaceMaterial = MI_Portal_Standard
│ └─ CaptureComponent.QualityProfiles[2].bEnableClipPlane = true
│ └─ Clip plane prevents geometry behind portal from clipping in
├─ Components:
│ ├─ SurfaceMesh (portal plane)
│ ├─ ProximityTrigger (quality scoring, large: 2000uu)
│ └─ TeleportTrigger (overlap detection, same size as surface)
└─ Event Graph:
├─ BeginPlay → EnableSurface → RegisterWithManager
├─ TeleportTrigger.OnComponentBeginOverlap
│ └─ Cast to PlayerCharacter → If valid:
│ └─ HasAuthority → Teleport player
│ └─ Client → Server_TeleportThroughPortal RPC
└─ Server_TeleportThroughPortal (RPC, Reliable, Server)
├─ Validate portal still active
├─ Get LinkedTargetSurface
├─ Teleport player to exit location
└─ Multicast_OnPlayerTeleported (play FX on all clients)
```
---
## 5. BP_FakeWindow_WardensOffice — Atmospheric Window
**Asset Path:** `Content/Game/Actors/BP_FakeWindow_WardensOffice.uasset`
**Parent:** `BP_FakeWindow` (143)
**Framework Systems:** 136-138, 143, 144-147
### Level Placement
```
L_Asylum_WardensOffice → Behind Warden's desk
├─ BP_FakeWindow_WardensOffice (large window frame)
├─ Shows: "Outside" view — rain, night sky, distant asylum building
├─ Weather: Rain overlay based on game state
└─ Parallax: Slight depth effect when player moves
```
### Behavior
```
BP_FakeWindow_WardensOffice
├─ Class Defaults:
│ ├─ CaptureComponent.CaptureMode = FakeWindow
│ ├─ CaptureComponent.CaptureFOV = 100 (wide window view)
│ ├─ CaptureComponent.QualityProfiles[0] = Low (256px, 4fps)
│ │ └─ Window is far from action — low priority
│ └─ SurfaceMaterial → MI_FakeWindow_Interior
├─ Weather Integration:
│ └─ Listen to GS_HorrorGameState.OnGlobalFearChanged
│ ├─ Fear < 0.3: Clear night, distant lights
│ ├─ Fear 0.3-0.6: Light rain, overcast
│ └─ Fear > 0.6: Heavy rain, lightning flashes, dark
└─ Sublevel Streaming:
└─ bStreamSublevelOnProximity = true
├─ Stream in when player within 3000uu
└─ Stream out when player beyond 4000uu
```
---
## Capture Integration Checklist (for Project Void)
### Mirrors
- [ ] Create `BP_Mirror_NurseStation` in WardA Nurses Station
- [ ] Create `BP_HorrorMirror_Morgue` in WardB Morgue
- [ ] Create `BP_HorrorMirror_WardensOffice` in WardensOffice
- [ ] Place `BP_Enemy_Shade_Reflection` (hidden) for morgue horror mirror
- [ ] Create `DA_Scare_MirrorApparition` Data Asset (references BPC_ScareEventSystem)
- [ ] Wire morgue mirror scare to BPC_ScareEventSystem.TriggerScareEvent()
- [ ] Wire audio through SS_AudioManager for mirror crack/steam/whisper sounds
### Monitors
- [ ] Create `BP_Monitor_Security` in WardA Security Office
- [ ] Create `BP_Monitor_Morgue` in WardB Morgue office
- [ ] Place `SecurityCamera_HallwayA` CameraActor in WardA hallway
- [ ] Wire monitor power to fusebox puzzle completion
### Portals
- [ ] Create `BP_Portal_VoidEntrance` in Basement Crematorium
- [ ] Create `BP_Portal_VoidExit` in VoidSpace (behind Memory Shrine 3)
- [ ] Wire portal visibility to Memory Shrine 3 completion
- [ ] Add BPC_StateManager.IsActionPermitted(Teleport) check before teleport
### Fake Windows
- [ ] Create `BP_FakeWindow_WardensOffice` in WardensOffice
- [ ] Create sublevel with window view room + camera
- [ ] Wire weather overlay to GS_HorrorGameState.GlobalFearLevel
### Quality & Performance
- [ ] Test 8 capture surfaces active simultaneously → verify budget enforcement
- [ ] Monitor GPU cost with `stat GPU` in PIE
- [ ] Verify mirrors go Off when player is far away
- [ ] Verify Hero tier assigned to mirror player is looking directly at
---
## Notes for Expansion
- Add **mirror puzzle**: player must look at specific angle in mirror to see hidden key reflected
- Add **portal network**: multiple interconnected portals create non-Euclidean maze
- Add **monitor hacking**: player can switch security cameras to find hidden items
- Add **weather system**: fake windows sync weather with game narrative progression
- Add **mirror memory**: some mirrors show "echoes" of past events (scripted timeline sequences)
- Multiplayer: portals teleport all players; mirrors render each client's own reflection
---
*Planar Capture Examples for Project Void. See [GAMEINDEX.md](GAMEINDEX.md) for full game structure. See [planar-capture-system.md](../architecture/planar-capture-system.md) for architecture spec.*