Foveated rendering

A headset spends most of its pixels where you cannot see detail. Foveation is the runtime shading those pixels less, and letting you say how aggressively.

Watch the periphery come apart

The panel is an offscreen render shown through a shader that snaps pixels to a coarser grid in each band outward from the centre. Push the slider and the outer bands block up while the middle stays sharp; the rings are where the steps happen.

The demo needs WebGL. The explanation below covers the same ground without it.

Starting the demo…

Where a headset wastes its pixels

The image a headset shows you is not evenly useful. Lenses are sharpest along the axis you are looking down and get progressively softer toward the edge, and the eye itself resolves fine detail only in a small central region. On top of that, the runtime has to distort the image to cancel the lens, which stretches the corners of the rendered buffer across fewer physical pixels than the middle.

So the corners of every eye buffer are pixels you paid full price for and can barely see. On a headset drawing two views at 90 frames a second, that is a large fraction of the shading budget going somewhere the user cannot appreciate it.

Foveation is the runtime shading those regions at a lower rate and scaling the result back up. Fixed foveation applies a fixed pattern based only on position in the buffer. Eye-tracked foveation moves the sharp region to wherever the user is actually looking, which is better but needs eye tracking hardware and introduces a latency requirement of its own.

Asking for it

The property sits on the layer, not on the session, because it describes how that buffer is shaded. You can set it once after creating the layer, or change it at runtime — some applications lower foveation on menus, where the user reads text near the edge of the display, and raise it during motion, where they will not notice.

const session = await navigator.xr.requestSession('immersive-vr');
const gl = canvas.getContext('webgl2', { xrCompatible: true });
const layer = new XRWebGLLayer(session, gl);
session.updateRenderState({ baseLayer: layer });

// 0 = no foveation, 1 = maximum. Anything in between is allowed.
layer.fixedFoveation = 0.5;

// Read it back: the runtime may not have used your number.
console.log(layer.fixedFoveation);

In three.js the same control is renderer.xr.setFoveation(value), which sets it on whichever layer the renderer is managing.

The number is a request

Reading the property back will not always return what you set. The specification describes the value as a hint: the runtime picks the pattern it actually supports, and hardware differs in how many shading-rate tiers it offers and where the boundaries fall. A device with three tiers cannot honour a request for a smooth ramp.

This matters for testing. If you set 0.5 on one headset and see a clear quality drop, then set the same 0.5 on another and see none, neither device is broken. Treat the value as a position on a dial whose detents you do not control, and judge the result by looking rather than by the number you wrote.

It also means the saving is not something you can calculate from the value. The only honest way to know what foveation bought you is to measure frame timing with it on and off, on the device you care about.

What the demo above is doing, and what it is not

The demo renders the inner scene to an offscreen target, then draws that target through a fragment shader that snaps texture coordinates to a grid. The buffer is split into four bands by distance from the centre, and each band uses one grid size: the middle samples every pixel, and each band outward doubles the block. The orange rings are the band boundaries, so what you see change at a ring is the whole of what changes.

That is a simulation of the visible consequence, not of the mechanism. Real fixed foveation changes the shading rate inside the GPU, and no desktop browser exposes that control. The reason the demo shows blocking rather than blur is that blocking is what a lower shading rate looks like once the result is scaled back up, and blur would suggest a filter that is not there. The bands are discrete for the same reason the previous section gives: hardware offers a handful of shading-rate tiers, not a smooth ramp.

The inner scene is deliberately full of high-frequency detail: a fine checker floor, a knotted torus with thin tubes, and rings of small spheres out at the edges. Low-frequency content survives heavy foveation almost untouched, which would leave the impression that the feature is free.

What breaks first

Foveation is close to invisible on some content and obvious on others. These are the cases that show it up, roughly in order of how early they fail.

Text near the edge of the view
Letterforms are high-contrast thin strokes, exactly what a lower shading rate destroys. A menu that sits in the periphery will look fine in the centre of the display and unreadable at the corner.
Thin bright geometry on a dark background
Wires, railings, particle trails and UI outlines alias badly when the shading rate drops. The eye catches the flicker even when it cannot resolve the line.
High-frequency textures
Fine repeating patterns such as grids, fabric weave and foliage turn into shimmering blocks under motion. A mip-biased texture often survives better than a sharp one.
Specular highlights
Small bright spots move between shading samples from frame to frame, so they blink. This is the failure most often mistaken for a lighting bug.
Anything the user will turn to look at
Fixed foveation does not know where the eye is pointed. Content the user is expected to inspect closely should not be parked in the periphery on the assumption that foveation is subtle.

Fixed versus eye-tracked

The two are often discussed as one feature. They have different requirements and different failure modes.

Fixed foveationEye-tracked foveation
Where the sharp region sitsCentre of the buffer, alwaysWherever the user is looking
Hardware neededNone beyond the GPUEye tracking, plus low-latency reporting
How aggressive it can beLimited, because the user may look anywhereMuch higher, because the periphery is genuinely unseen
Main failure modePeripheral detail the user turns to inspectTracking loss or lag, which briefly softens what is being looked at
Web APIXRWebGLLayer.fixedFoveationNot exposed to the web platform

Checked against the WebXR Device API specification and three.js documentation, 2026-09. Eye-tracked foveation exists in native runtimes on some devices; it has no WebXR surface as of this date.

Mistakes that cost the most time

Treating it as a quality setting for users
A slider labelled "foveation" in your own settings menu asks the user to trade sharpness they can see for a frame rate they may not be missing. Set it yourself, per scene, and measure.
Setting it before the layer exists
The property lives on XRWebGLLayer. Setting it on the session, or before updateRenderState has taken effect, silently does nothing.
Assuming the read-back value is your value
The runtime may quantise or ignore the request. Code that branches on the value it just wrote will take the wrong branch on some devices.
Measuring on a machine that was not GPU-bound
Foveation saves fragment shading. If the frame is limited by draw calls, geometry or CPU work, turning it up changes nothing and you will conclude it does not help.

Further reading