12 KiB
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 │
│ USS_PlanarCaptureManager ← Global budget, RT pool, scoring (WorldSubsystem + FTickable) │
│ 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
- Registry:
BP_Mirror.BeginPlay()callsSS_PlanarCaptureManager.RegisterSurface(this)— the mirror is now tracked globally. - Evaluation (0.5s interval):
SS_PlanarCaptureManager.Tick()callsEvaluateAllSurfaces(). - 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
- Tier Assignment: Based on composite score and budget limits, the manager calls
BPC_PlanarCapture.ApplyQualityTier(Tier). - Capture Tick (per frame):
BPC_PlanarCapture.TickComponent()checks time since last capture vs the quality tier'sCaptureInterval. If it's time to capture:- Gets the player camera transform
- Calls
ComputeCaptureCameraTransform()— for Mirror mode, this callsUPlanarCaptureCameraUtils::ComputeMirroredTransform()which reflects the player camera across the mirror plane - Sets the
USceneCaptureComponent2Dworld transform - Calls
SceneCapture->CaptureScene()— renders to the allocatedUTextureRenderTarget2D - Calls
PushMPCParameters()to update the MPC with current horror/visual params
- Material Display:
M_CaptureSurface_Mastersamples 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:
- 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. - Total Memory Budget:
MaxTotalRenderTargetMemoryMB(default 128MB). If exceeded, the manager logs a warning. Future: automatic demotion of lowest-priority surfaces until under budget. - 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)
// Portal teleport must be gated
if (!StateManager->IsActionPermitted(FGameplayTag::RequestGameplayTag("Framework.Action.Teleport")))
return; // Blocked — player is dead, in cutscene, etc.
BPC_ScareEventSystem (101)
// Horror mirror triggers coordinated scare
ScareEventSystem->TriggerScareEvent(ScareEventTag);
// This coordinates lights (96), audio (132), pacing (98), stress (10)
SS_AudioManager (132)
// All surface audio routes through audio subsystem
AudioManager->PlaySoundAtLocation(MirrorShatterCue, GetActorLocation());
AudioManager->PlaySoundAtLocation(PortalWhooshCue, GetActorLocation());
I_Persistable (36)
// Surface state persists across saves
// BP_PlanarCaptureActor implements I_Persistable:
CollectState() → { bIsDestroyed, CurrentDirtLevel, CurrentSteamLevel }
RestoreState() → { Apply saved state }
SS_EnhancedInputManager (128)
// Portal transition switches input context
InputManager->PushContext(PortalTransitionContext, EInputContextPriority::Inspection);
// Player exits portal → PopContext
Multiplayer Networking
What Replicates
bRepIsActiveonBP_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
- MaxCaptureDistance: Set lower (3000-5000) for indoor levels — mirrors far away go Off
- FullEvaluationInterval: Increase to 1.0s for large levels with many mirrors to reduce CPU scoring cost
- GlobalQualityCap: Set to Medium on Steam Deck. Set to High on consoles.
- ShowOnly lists: Use aggressively. Capturing only 5 actors is vastly cheaper than capturing 500.
- Monitor FPS: Monitors should use 5fps Low tier — they don't need real-time updates.
- 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/.