The Problem: Scopes Kill VR Frame Rates
Every VR shooter needs weapon scopes. But the standard Unreal Engine approach - SceneCaptureComponent2D - renders
the entire scene a second time every frame. In VR, where you already render two eyes at 90 Hz, that extra scene render
can easily push you over budget and cause reprojection.
Common workarounds all have tradeoffs:
| Approach | Problem |
|---|---|
| SceneCaptureComponent2D | Full scene render every frame. 5-15ms GPU cost in VR. |
| Fake scope (zoomed overlay) | Breaks immersion, no parallax, can't see around the scope. |
| SceneCapture with timer | Manual tick management, no LOD control, still renders everything. |
| Stencil masking | Complex setup, Z-fighting, no magnification. |
The Solution: RVR Scope API
RVR's Scope View is a purpose-built rendering pipeline for weapon optics. Instead of capturing the full scene at full quality every frame, it renders a lean, low-resolution view at a controlled frame rate with aggressive performance defaults.
What makes it different from SceneCaptureComponent2D:
- ✓ Tick-gated rendering - only renders at the capture FPS you set (default 20 Hz), not every game frame
- ✓ Aggressive LOD bias - at 512px width, objects use LODs as if 2.5x farther away
- ✓ Reduced far clip - culls distant geometry before vertex shading
- ✓ Stripped show flags - disables translucency, particles, foliage, motion blur, DoF, SSR, AO by default
- ✓ FXAA anti-aliasing - single-frame, no temporal dependency (TAA requires history buffers)
- ✓ Screen percentage - default 50%, so a 512x512 target actually renders at 256x256 internally
- ✓ PF_B8G8R8A8 render target - 32bpp instead of PF_FloatRGBA (64bpp), halving memory bandwidth
- ✓ Warmup spreading - renders one warmup frame per tick to avoid a hitch on the first frame
Quick Start: Blueprint
The minimal setup is a single Blueprint node. Attach a CameraComponent to your weapon scope mesh,
set a narrow FOV (10-20 degrees for magnification), then call StartScopeView.
// In your weapon Blueprint (e.g. BeginPlay or ADS event):
1. Get RVR subsystem: Get Engine Subsystem -> Runtime Video Recorder
2. Call StartScopeView:
- ScopeCamera: your scope CameraComponent (FOV = 12 for 6x magnification)
- Width: 512
- Height: 512
- CaptureFPS: 20
- ScreenPercentage: 50.0
3. Take the returned Render Target
4. Set it as a texture parameter on your scope lens material:
- Create Dynamic Material Instance (scope lens mesh)
- Set Texture Parameter Value ("ScopeTexture", returned RT)
When the player exits ADS or holsters the weapon, call StopScopeView to release the render target and stop the extra renders.
C++ Implementation
Full C++ setup for a VR sniper rifle scope:
// WeaponScope.h
UPROPERTY(VisibleAnywhere)
UCameraComponent* ScopeCamera;
UPROPERTY()
UMaterialInstanceDynamic* ScopeLensMaterial;
void ActivateScope();
void DeactivateScope();
// WeaponScope.cpp
void AWeaponScope::ActivateScope()
{
auto* RVR = GEngine->GetEngineSubsystem<URuntimeVideoRecorder>();
if (!RVR) return;
FRuntimeScopeRenderSettings RenderSettings;
RenderSettings.MaxScopeDistance = 80000.f; // 800m for a sniper
RenderSettings.bEnableDynamicShadows = true;
RenderSettings.bEnableFog = true;
RenderSettings.bEnableAtmosphere = true;
// Everything else defaults to OFF
UTextureRenderTarget2D* ScopeRT = RVR->StartScopeView(
ScopeCamera,
512, // Width
512, // Height
20, // CaptureFPS
50.0f, // ScreenPercentage
FRuntimeSelectiveRenderingSettings(),
nullptr, // No custom post-process
RenderSettings
);
if (ScopeRT && ScopeLensMaterial)
{
ScopeLensMaterial->SetTextureParameterValue(
TEXT("ScopeTexture"), ScopeRT);
}
}
void AWeaponScope::DeactivateScope()
{
auto* RVR = GEngine->GetEngineSubsystem<URuntimeVideoRecorder>();
if (RVR) RVR->StopScopeView();
}
StartScopeView API Reference
| Parameter | Type | Default | Notes |
|---|---|---|---|
| ScopeCamera | UCameraComponent* | required | Set a narrow FOV for magnification (e.g. 12 = ~6x zoom) |
| Width / Height | int32 | 512 | Render target resolution. Lower = faster. 256 works for close-range optics. |
| CaptureFPS | int32 | 20 | How often the scope renders. 20 Hz is visually smooth for a scope view. |
| ScreenPercentage | float | 50.0 | Internal render scale (5-100). At 50%, a 512 target renders at 256 internally. |
| SelectiveRendering | FRuntimeSelectiveRenderingSettings | empty | ShowOnlyActors / HiddenActors arrays for filtering what the scope sees. |
| CustomPostProcessMaterial | UMaterialInterface* | nullptr | Crosshair overlay, vignette, night vision, thermal - applied as post-process. |
| ScopeRenderSettings | FRuntimeScopeRenderSettings | see below | Fine-grained toggle for every rendering feature. |
Returns a UTextureRenderTarget2D* that you assign to your scope mesh material. Only one scope view can be active at a time; calling again stops the previous one.
FRuntimeScopeRenderSettings - What's On and Off
Every feature defaults to OFF for maximum VR performance. Enable only what you need:
| Feature | Default | When to Enable |
|---|---|---|
| MaxScopeDistance | 50000 cm (500m) | Increase for sniper rifles, decrease for red-dot sights |
| Dynamic Shadows | ON | Critical for depth perception. Disable only on Quest 2. |
| Contact Shadows | OFF | Barely visible at scope resolution. Not worth the cost. |
| Capsule Shadows | OFF | Enable if characters look like they float. |
| Translucency | OFF | Enable if you need glass, smoke shells, or transparent surfaces. |
| Particles | OFF | Enable for muzzle flash and tracer visibility through scope. |
| Foliage | OFF | Extremely expensive at scope resolution. Rarely needed. |
| Decals | OFF | Enable for bullet-hole visibility. |
| Fog / Atmosphere | ON | Cheap and helps depth perception at distance. |
| Bloom / DoF / Motion Blur / Lens Flares / Volumetric Fog / AO / SSR | OFF | All post-process effects off. A scope is a rigid optic - no motion blur or DoF. |
Performance Under the Hood
Here's what the Scope API does behind the scenes to keep costs minimal:
Tick-Gated Rendering
The scope only renders when the capture interval elapses. At 20 FPS, that's once every 50ms. Your game's 90 Hz VR loop runs uninterrupted for the other frames. SceneCaptureComponent2D renders every single frame.
Aggressive LOD Bias
LOD distance factor is calculated as 1280 / Width, clamped between 1.0 and 8.0. At 512px, that's 2.5x - objects pick LODs as if they were 2.5x farther away. This dramatically reduces polygon count without visible quality loss at scope resolution.
Reduced Far Clip Plane
MaxScopeDistance (default 500m) rejects distant geometry before vertex shading even begins. The main camera still sees the full world, but the scope only processes what's within range.
Manual Exposure
Auto-exposure is forced off. The scope uses manual exposure to prevent brightness flickering as you pan across different areas. This also skips the luminance compute pass.
Warmup Frame Spreading
When the scope activates, warmup frames are spread across multiple ticks (one per tick) instead of rendering all at once. This prevents a frame spike when the player first aims down sights.
Custom Post-Process: Crosshair, Night Vision, Thermal
Pass a CustomPostProcessMaterial to overlay a crosshair reticle, add a vignette, or build a full night-vision/thermal scope.
When a custom material is provided, the view mode switches to Unlit with FinalColorLDR capture, and your material receives the scene as input.
// Create a night-vision post-process material in your asset pipeline,
// then pass it to StartScopeView:
UMaterialInterface* NightVisionMat = LoadObject<UMaterialInterface>(
nullptr, TEXT("/Game/Materials/MI_NightVision"));
RVR->StartScopeView(
ScopeCamera, 512, 512, 20, 50.0f,
FRuntimeSelectiveRenderingSettings(),
NightVisionMat, // Applied as post-process
FRuntimeScopeRenderSettings()
);
The material gets the full scope scene as a texture input. Use it to add green tint + noise for night vision, or remap colors for a thermal overlay.
Selective Rendering: Hide or Show Specific Actors
Use FRuntimeSelectiveRenderingSettings to control what the scope sees. This is useful for:
- - Hiding the player's own weapon mesh from the scope view
- - Showing only tagged "enemy" actors for a wall-hack scope power-up
- - Filtering out UI/HUD elements
FRuntimeSelectiveRenderingSettings Selective;
Selective.HiddenActors.Add(PlayerWeaponActor);
Selective.HiddenActors.Add(PlayerHandsActor);
RVR->StartScopeView(
ScopeCamera, 512, 512, 20, 50.0f,
Selective,
nullptr,
FRuntimeScopeRenderSettings()
);
Events and Delegates
RVR broadcasts two delegates for scope lifecycle:
OnScopeViewStarted- fires when the scope render target is created and readyOnScopeViewStopped- fires when the scope is deactivated and the render target released
Bind to these to show/hide a scope overlay widget, play an ADS animation, or switch the crosshair style.
Helper Methods
IsScopeViewActive()- returns true if a scope view is currently renderingGetScopeViewRenderTarget()- returns the active render target (or nullptr)StopScopeView()- stops rendering and releases the render target
The scope is automatically stopped on EndPlay and Deinitialize, so you don't need to manually clean up when the level ends.
Tuning Guide: Quest vs PCVR
Quest 2/3 (Mobile VR)
- - Resolution: 256x256
- - CaptureFPS: 15
- - ScreenPercentage: 30-40%
- - Dynamic Shadows: OFF
- - MaxScopeDistance: 30000 (300m)
- - All post-process: OFF
PCVR (Index, Vive Pro)
- - Resolution: 512x512
- - CaptureFPS: 30
- - ScreenPercentage: 50-70%
- - Dynamic Shadows: ON
- - MaxScopeDistance: 80000 (800m)
- - Fog + Atmosphere: ON
Tip: Start with the defaults (512, 20 FPS, 50%) and profile with stat gpu.
The scope render should cost under 1ms on PCVR and under 2ms on Quest 3.