reckfellENGINE

XR fullscreen vignette

A working shader without screen reads, per-view projection, quad visibility and diagnosis.

On this page

XR fullscreen vignette

This method overlays a black transparent mask on the 3D view without reading screen color or depth. It suits a vignette; this shader performs no blur, color grading or depth-based effect. For buffer access, use Compositor, accounting for stage support in the selected renderer.

First set up a working XR rig. The effect neither enables OpenXR, creates tracking nor replaces focus-loss handling. You can also check it on a regular Camera3D without a headset.

Prepare the surface

  1. Add a MeshInstance3D child Entity to XRCamera3D. Name it Vignette and set Position=(0, 0, −1).
  2. Create a QuadMesh with Size=(2, 2). Its X/Y vertices range from −1 to 1. No surface subdivisions are needed: the shader writes clip-space coordinates directly.
  3. Assign a ShaderMaterial to MaterialOverride, create its Shader and paste the code below. Set the material RenderPriority to 127. This is a high transparent-material priority, not a promise to draw over any UI.
  4. Set MeshInstance3D.CastShadow=Off. To prevent CPU frustum culling, use CustomAabb: Position=(−1000, −1000, −1000), Size=(2000, 2000, 2000). These bounds keep the pass visible; they do not shape the mask geometry.
  5. If the project uses occlusion culling, enable IgnoreOcclusionCulling on this Entity. A large CustomAabb and exclusion from occlusion culling solve different problems; disabling depth testing does not disable CPU culling.

Do not adjust coverage through Entity scale: the code uses original VERTEX.xy as clip coordinates, bypassing the usual geometry transform. QuadMesh.Size determines coverage, not distance from the camera. Camera parenting and CustomAabb handle lifetime and CPU visibility.

Shader

Shader
shader_type spatial;
render_mode unshaded, depth_test_disabled, depth_draw_never,
    cull_disabled, fog_disabled, shadows_disabled;

uniform float strength : hint_range(0.0, 1.0) = 0.6;
uniform float inner_radius : hint_range(0.0, 2.0) = 0.55;
uniform float outer_radius : hint_range(0.0, 2.0) = 1.15;
varying vec2 clip_position;
varying vec2 forward_center;

void vertex() {
    clip_position = VERTEX.xy;
    vec4 forward_clip = PROJECTION_MATRIX * vec4(0.0, 0.0, -1.0, 1.0);
    forward_center = forward_clip.xy / forward_clip.w;
    POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}

void fragment() {
    float radius = length(clip_position - forward_center);
    float end_radius = max(outer_radius, inner_radius + 0.001);
    ALBEDO = vec3(0.0);
    ALPHA = clamp(strength, 0.0, 1.0)
        * smoothstep(inner_radius, end_radius, radius);
}

strength=0 makes the mask transparent; strength=1 permits full outer darkening. inner_radius sets the clear central area; outer_radius ends the transition. If the outer radius is no greater than the inner one, the code retains a minimum transition width of 0.001. To disable the pass entirely, hide Vignette with Visible=false: a transparent shader does not itself remove the draw call.

Why the center depends on projection

With an asymmetric projection, the forward direction need not land at the center of the rectangular eye texture. Measuring only from UV=(0.5, 0.5) can therefore shift the visible vignette away from the forward direction.

The code projects a camera-axis point (0, 0, −1, 1), divides X/Y by W and obtains the center in NDC. Each fragment measures distance from that center in the same coordinate system. In RD multiview, the current implementation takes PROJECTION_MATRIX from the corresponding view matrix. This was reviewed in source; the execution did not render a real pair of eyes.

Radii use NDC, not meters or degrees. The same radius does not guarantee an equal angular field across headsets. This is neither lens calibration nor a guaranteed motion-sickness remedy. Assess the shape and transitions on the target device; do not obscure required indicators, menus or warnings.

Cost and limitations

The shader uses ALPHA, so the quad enters the transparent pass and touches the screen area of each view. It has no screen/depth sampler: this mask requests no separate screen-texture read. Draw calls, fragment calculations, blending and output writes still remain. Do not treat the effect as free.

Reading screen color in another variant requires an available snapshot/intermediate buffer; additional copies and effects on optimizations depend on the renderer. Do not assume that variant costs the same as this mask, or promise that every screen read disables absolutely all XR optimizations. Check VRS, MSAA and GPU time separately on the target runtime.

The overlay changes no depth buffer, does not darken separately composited OpenXR layers and does not guarantee coverage of CanvasLayer/UI drawn later. If you need a post effect over all layers, establish their composition order before raising RenderPriority blindly.

Diagnose an incorrect result

SymptomCheck
Mask disappears when turningXRCamera3D parenting, CustomAabb, occlusion culling and Visible; writing POSITION does not disable CPU culling.
Mask does not cover the screenQuadMesh.Size=(2, 2), original X/Y from −1 to 1 and the exact POSITION assignment above.
Geometry covers the maskdepth_test_disabled, ALPHA and transparent-material priority.
Center shifts in the headsetEach eye projection, no fixed UV center, and the distinction between NDC and angular field; compare both eyes.
No fully clear areastrength, inner_radius and ShaderMaterial uniform overrides.
UI remains brightCanvasLayer and composition-layer order: this pass operates inside the 3D view.

Verification scope

The exact shader above ran on installed Reckfell 1.0.0, Windows/Vulkan, Forward+ and Mobile. Pixel readback confirmed a clear center, a dark edge, strength-based disabling, centering under a synthetic horizontally asymmetric projection and coverage after camera movement. These were small diagnostic buffers, not graphics-quality illustrations. The Vulkan loader warning remains in the report.

OpenXR sessions, headset multiview, lenses, device vertical asymmetry, comfort, GPU cost, other drivers, export and editor UI were not tested. Check both eyes and menus on the target headset before release; a synthetic Camera3D does not replace that check.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.