注视点渲染

头显把大部分像素花在了你根本看不清细节的地方。注视点渲染就是运行时少给那些像素着色,并让你说要少给多少。

看着外围散掉

这块面板是一张离屏渲染的画面,再经过一个 shader 显示:从中心往外每一个环带,像素被吸附到更粗的格子上。拉动滑块,外圈会块状化而中间保持锐利;那几道环就是阶梯发生的位置。

演示需要 WebGL。下面的文字不依赖它,覆盖同样的内容。

正在启动演示…

头显把像素浪费在哪儿

头显给你看的那幅图,各处的有用程度并不一样。镜片在你视线正对的轴向上最锐利,越往边缘越软;而眼睛本身也只在中央一小块区域能分辨细节。再加上运行时必须把画面做反畸变去抵消镜片,这会把渲染缓冲的四角拉伸到比中间更少的物理像素上。

于是每只眼睛缓冲区的四角,都是你按全价付了钱、却几乎看不见的像素。一台要以每秒 90 帧画两个视图的头显,这部分占掉的着色预算相当可观,而用户完全感受不到。

注视点渲染就是运行时对这些区域用更低的着色率,再把结果放大回去。固定注视点只按位置套一个固定的模式;眼动追踪注视点则把锐利区移到用户真正在看的地方,效果更好,但需要眼动硬件,也带来自己的延迟要求。

怎么开

这个属性挂在 layer 上而不是 session 上,因为它描述的是**那块缓冲区怎么着色**。你可以在创建 layer 之后设一次,也可以运行时改:有些应用在菜单界面把强度调低,因为用户会去读靠近边缘的文字;而在剧烈运动时调高,因为那时候没人会注意到。

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 = 不做注视点渲染,1 = 最强。中间任意取值都合法。
layer.fixedFoveation = 0.5;

// 读回来看看:运行时未必用了你给的那个数。
console.log(layer.fixedFoveation);

在 three.js 里对应的是 renderer.xr.setFoveation(value),它会设到渲染器正在管理的那个 layer 上。

那个数字只是一个请求

把这个属性读回来,未必等于你写进去的值。规范把它描述成一个提示:运行时会挑自己真正支持的模式,而不同硬件支持的着色率档位数量和分界点都不一样。只有三档的设备,没法满足一个要求平滑过渡的请求。

这一点对测试很要紧。你在一台头显上设 0.5 看到明显的画质下降,在另一台上设同样的 0.5 却什么都看不出来,这两台设备都没坏。把这个值当成一个刻度盘上的位置,而那些卡位不由你控制;判断结果要靠眼睛看,不是靠你写进去的数。

这也意味着**省下了多少,没法从这个值算出来**。唯一诚实的办法是在你真正关心的那台设备上,开和关各测一次帧时间。

上面那个演示在做什么,以及它不是什么

演示先把内层场景渲染到一张离屏纹理,再通过一个片元着色器把它画出来:着色器把纹理坐标吸附到格子上。缓冲区按离中心的距离分成四个环带,每一带用同一个格子尺寸——中间逐像素采样,往外每一带把方块边长翻倍。那三道橙色环就是分界线本身,所以你在环上看到的变化,就是全部的变化。

这是对**可见后果**的模拟,不是对机制的模拟。真正的固定注视点渲染改的是 GPU 内部的着色率,而桌面浏览器不暴露那个开关。演示之所以画成块状而不是模糊,是因为降低着色率之后再放大回去看到的就是块状;画成模糊会暗示存在一个并不存在的滤波。分成离散的环带也是同一个理由,上一节说过:硬件提供的是有限的几档着色率,不是平滑斜坡。

内层场景刻意堆满了高频细节:很密的棋盘地面、管子很细的环面纽结、以及边缘那几圈小球。低频内容在很强的注视点渲染下几乎毫发无伤,那会让人误以为这个特性是免费的。

什么最先崩

注视点渲染在有些内容上几乎看不见,在另一些上一眼就能看出来。下面这些是会露馅的情况,大致按「多早失效」排列。

靠近视野边缘的文字
字形是高对比度的细笔画,恰恰是降低着色率最先毁掉的东西。一个摆在外围的菜单,在画面中央看着没问题,到了角落就不可读。
深色背景上的细亮几何
线缆、栏杆、粒子拖尾、UI 描边,在着色率下降时会严重锯齿化。即使眼睛分辨不出那根线,也能捕捉到它的闪烁。
高频纹理
网格、织物纹理、树叶这类细密重复图案,在运动中会变成闪烁的方块。做过 mip 偏置的纹理往往比锐利的那张更抗得住。
高光
小而亮的高光点会在相邻帧之间在着色样本间跳动,于是一闪一闪。这是最常被误认成光照 bug 的一种失效。
任何用户会转头去看的东西
固定注视点渲染不知道眼睛指向哪里。指望用户仔细端详的内容,不该因为「注视点渲染很微妙」就被放在外围。

固定式与眼动式

这两者常被当成一个特性来谈。它们的前提条件和失效方式都不一样。

固定注视点眼动追踪注视点
锐利区在哪永远在缓冲区中央用户看哪儿就在哪儿
需要什么硬件除 GPU 外不需要眼动追踪,外加低延迟上报
能做到多激进有限,因为用户可能看向任何地方可以高得多,因为外围是真的没被看到
主要失效方式用户转头去看的那块外围细节追踪丢失或滞后,会短暂糊掉正在看的地方
Web APIXRWebGLLayer.fixedFoveation未向 Web 平台暴露

依据 WebXR Device API 规范与 three.js 文档核对于 2026-09。眼动追踪注视点在部分设备的原生运行时里已有,但截至此日期没有对应的 WebXR 接口。

最费时间的几个坑

把它当成给用户的画质选项
在自己的设置菜单里放一个叫「注视点渲染」的滑块,等于让用户拿他看得见的锐利度,去换一个他可能根本没觉得缺的帧率。这个值应该由你按场景自己定,并且实测。
在 layer 存在之前就设
这个属性属于 XRWebGLLayer。设在 session 上、或者在 updateRenderState 生效之前设,都会静默地什么也不做。
以为读回来的就是你设的
运行时可能量化或忽略你的请求。根据「刚写进去的值」去分支的代码,在某些设备上会走错分支。
在一台本来就不是 GPU 瓶颈的机器上测
注视点渲染省的是片元着色。如果这一帧卡在绘制调用、几何或 CPU 上,把强度调满也不会有变化,然后你会得出「它没用」的结论。

延伸阅读