如果你戴上头显转头,发现画面慢了那么一丁点,才会意识到“实时”这个词在虚拟现实里有多苛刻。实时虚拟现实技术理论这门课讲到这里,已经从前五次的视觉感知、交互基础,推进到第六次真正硬核的话题:渲染与延迟之间的关系。这次的课堂笔记,核心是一条经常被提起但很少被讲透的链路——运动到光子延迟,再配合姿态预测、异步时间扭曲、双眼渲染优化这些配套手段。对正在做VR应用开发的人,或者想弄明白“为什么VR非要90帧以上”的朋友,这篇笔记应该可以直接回答你的困惑。
1. 这门课真正想解决的问题:不是“流畅”,而是“一致”
先说一个反直觉的结论:在虚拟现实里,“流畅”这个词本身是误导人的。普通3D游戏,画面30帧甚至20帧也能玩,眼睛盯着固定的显示器,镜头移动是由自己操作的,大脑可以容忍一定的顿挫。但VR里情况完全不同,头部转动时,大脑的平衡系统会立刻给出一个“我在转头”的信号,如果画面里的视角没有同步跟上,大脑同时收到“位置变了”和“视野没变”两个矛盾信息,晕眩感就来了。
所以课程的第六次笔记,把我们拉回到一个更本质的指标上——运动到光子延迟。它指的是从用户头部真正发生运动,到屏幕上对应像素更新出这个运动结果为止的完整时间。这不仅仅是引擎渲染一帧需要多少毫秒,而是跨越传感器采样、数据传输、应用逻辑、CPU提交、GPU渲染、扫描输出、像素响应的一条完整链路。
很多入门资料喜欢强调“刷新率90Hz以上”就够了,但把时间轴拆开就会发现,90Hz只是意味着每帧间隔大概11ms。问题在于,这11ms只是渲染帧的理想周期,传感器不会在你需要的那一秒才上报数据,应用的逻辑也要消耗时间,更麻烦的是GPU工作完成后画面不是立刻显示到屏幕上,而是要先进入显示队列,等垂直同步信号到来。把这些都加在一起,哪怕是90Hz的头显,端到端延迟依然可能达到30到50ms。
课堂里老师给了一个经验判断:**低于20ms用户基本感受不到错位,20到40ms开始有明显延迟感,超过50ms大多数人都会产生晕动病的前兆。**这组数字不是一个边界明确的定理,但它解释了为什么实时VR技术不能只靠把分辨率做高、把画面做漂亮来解决问题。真实要面临的挑战是:如何在极短时间预算里,让虚拟视角和物理姿态保持一致。
1.1 先明确两个“流畅”的区别
一种流畅是“画面不卡”,即帧率稳定,渲染不抖。另一种流畅是“姿态变化和画面变化同步”,也就是感官一致性。绝大多数非VR图形学训练里,讨论的是前一种,但VR工程里必须同时兼顾两种。如果一个应用帧率很稳定,但总延迟恒定在60ms,表现出来就是画面不卡但“带着”一种慢半拍的感觉,转动头部时像在拖一块很重的镜片,这种体验比偶尔掉帧更让人难受。
理解了这层区别,后面很多技术选择就有了判据。比如为什么要用异步时间扭曲?为什么要限制渲染队列深度?为什么哪怕牺牲一点画质也不能让渲染超过帧预算?因为所有这些操作都在为“姿态到显示屏的一致性”服务,而不是单纯为了帧数数字好看。
1.2 用“运动到光子延迟”代替笼统的“卡顿”
排查VR性能问题时,只盯着“平均帧率”很容易被误导。在一次快速甩头过程中,单个帧可能还是保持在90fps,但这帧的姿态数据是20ms之前采样的,显示出来依然会拖影。正确的思路是把“运动到光子延迟”拆成几个可测量的阶段,看看时间到底花在哪里。
所以课堂笔记六开篇给了我们一个操作建议:不要用FPS仪表盘做VR体验评估,要记录时间线。记录姿态事件何时发生、应用何时拿到姿态、渲染何时完成、帧何时被扫描出来。这四个时间戳之间的差值,才是直接影响体验的东西。这次笔记后续的内容,基本都是在围绕这个时间线展开。
2. 延迟链路拆解:每一毫秒都是从哪里丢的
既然要控制运动到光子延迟,第一步就是把这条链路从头到尾摆到桌面上。我根据课程内容和自己的工程经验,整理了一张大致的延迟构成表。
| 环节 | 典型耗时 | 说明 |
|---|---|---|
| 惯性传感器采样 | 1~3ms | 头显姿态通常由加速度计和陀螺仪融合,采样频率越高、延迟越低 |
| 数据读取与传输 | 1~4ms | 传感器数据通过总线或无线协议上报,受刷新率和驱动调度影响 |
| 应用逻辑与物理模拟 | 3~8ms | 每帧内更新场景状态、处理输入事件,与逻辑复杂度有关 |
| CPU提交渲染命令 | 1~4ms | 创建命令缓冲区,把场景数据推给GPU,等待GPU执行 |
| GPU渲染 | 4~12ms | 几何处理、光栅化、着色,这是最常超时的环节 |
| 队列等待与垂直同步 | 0~16ms | 帧完成后未必立刻显示,要等显示设备的下一个刷新周期 |
| 屏幕扫描输出 | 0~8ms | 面板逐行或分区刷新,不同区域被点亮的时间有先后 |
| 像素响应时间 | 1~5ms | 液晶分子或OLED单元真正改变亮度所需的时间 |
这些时间不是简单相加,因为某些阶段可以重叠运行。比如CPU正在提交第N帧时,GPU可能还在渲染第N-1帧,显示器则在显示第N-2帧。所以一个60Hz显示设备上的真实延迟,并不等于“16.7ms + 所有环节时间”,而是取决于流水线深度和同步策略。实际项目里用高速相机去拍头显屏幕和追踪器上的标记,才能测到最真实的结果。
2.1 一张延迟构成表,你就能算出总预算
把上面的数值取一个保守的中间值算一下:传感器采样2ms,传输2ms,应用逻辑5ms,CPU提交3ms,GPU渲染8ms,垂直同步等待5ms,扫描输出4ms,像素响应3ms,加起来已经到32ms。这还算没有掉帧、没有额外缓冲的理想情况。
如果应用把渲染队列设成2到3帧,GPU后面还会积压整整几个帧周期,延迟会直接跳到50到80ms。普通游戏里这种队列没什么问题,因为用户看不到自己的姿态与画面的错位,但在VR里,这个队列就是晕眩感的放大器。因此很多头显运行时会强制把应用提交的帧限制为“当前正在渲染的帧 + 待显示的一帧”,有些模式甚至要求应用必须锁步到垂直同步,不允许自由缓冲。
2.2 队列、扫描相位和抖动:三个容易被忽视的来源
第一个来源是队列深度。帧队列越长,延迟越高,这个最容易理解。第二个来源是扫描相位。显示面板刷新时,并不是整块屏幕同时被点亮,而是从上到下逐行刷。如果垂直同步信号发生在画面底部,那么画面底部的内容会比顶部晚将近一个刷新周期。部分头显会配合高刷新率面板和全局刷新技术来减弱这个问题,但工程上仍然要留意。
第三个来源更隐蔽,叫做抖动。延迟的均值也许只有25ms,但如果每一帧的延迟在20ms到35ms之间来回波动,大脑对运动的预测会被反复打乱,造成的晕眩甚至比稳定的35ms更严重。这也是为什么很多VR开发工具里,除了平均帧率,还要看“帧完成时间的一致性”和“帧间隔的抖动分布”。
3. 姿态预测、外推与异步时间扭曲:工程上的三张拖延牌
既然延迟不可能真正压缩到零,工程上就发展出一套“以时间换感知”的策略。核心思路是:渲染需要时间,但我可以猜测用户未来一小段时间里会看向哪里,然后按这个猜测来渲染;或者先把参考帧渲染好,等真正显示前一刻再用最新姿态把画面拉回正确位置。姿态预测和异步时间扭曲,就是这么来的。
3.1 预测不是玄学:短时外推为什么有效
姿态预测的基础是物理惯性。头部转动受肌肉和颈椎驱动,在几十毫秒尺度上——注意,是几十毫秒,不是几百毫秒——运动轨迹相对平滑,角速度变化不会太剧烈。惯性测量单元以很高的频率(常见500Hz到1kHz)采样陀螺仪角速度,当应用需要渲染一帧时,已经能拿到最近几毫秒的真实角速度。于是一个简单的线性外推就能工作:
预测姿态 = 采样姿态 + 角速度 * 外推时间这里面最关键的是“外推时间”。如果外推长度是2ms,结果非常准;如果外推长度涨到20ms,角速度的积分误差和传感器噪声就会让姿态出现明显漂移。所以预测只能弥补链路中一两个环节的延迟,不能替代高帧率本身。作出预测时还需要做球面线性插值处理,不能直接拿欧拉角做加法,这是实现细节里最容易踩的坑。
3.2 时间扭曲只救姿态,救不了场景信息
异步时间扭曲做的事情更聪明:左右眼画面已经渲染完成,但还没等到垂直同步信号时,用最新的头部姿态把画面做一次重新投影。这本质上是一个全屏后处理四边形,运行开销固定,不依赖场景复杂度。旋转造成的视角改变可以用简单的纹理重采样得到,效果尤其好,因为它能把“从传感器到屏幕”的最后一段延迟补掉。
但这里有一个必然的代价:时间扭曲只改变已有像素的投射位置,不能生成原本看不到的信息。头转动时原本被遮挡的侧面、背后区域,画面里根本没有像素,扭曲只能在现有颜色和深度数据上瞎猜。平移比较大的情况下,边缘会出现黑边、拉伸或鬼影。所以高级实现会把深度缓冲也用上,让扭曲时能做一定的视差补偿,但深度不连续的位置依然会穿帮。
再往后发展出来的异步空间扭曲,则是当渲染帧率达不到刷新率时,直接在旧帧基础上合成一个中间帧。等于用前两帧的几何信息做插值,让显示频率比渲染频率更高。这类技术对抗帧率不足挺有效,但会在运动物体边缘留下半透明残影,所以只适合作为掉帧兜底,不适合当作常规优化手段。
4. 双眼渲染与可见性:为什么不能简单地画两遍
延迟预算被压缩后,下一件躲不开的事就是渲染负载。VR需要同时输出左右眼两个视角,直观做法是场景画两遍,一个眼睛一遍。这在早期确实是很多引擎的默认方案,但开销太粗暴:顶点着色、像素着色、提交批次全部翻倍,帧预算一下就被吃满。
工程上的第一个升级是单通道立体渲染。在支持实例化的图形接口里,把左右眼的视图投影矩阵打包进同一个常量缓冲区,提交画一个物体时,顶点着色器同时处理两个视角的输出。本质上一次绘制调用可以生成两套顶点位置,再输出到纹理数组的不同切片。这样CPU侧的提交开销几乎不变,GPU侧也避免了完全重复的顶点计算,整体效率比单纯画两遍高很多。
4.1 单通道立体渲染:把两次绘制变成一次
实现思路上,关键是不要把左右眼当成两个独立相机去遍历。更常见的做法是设置一个“双视图”渲染状态,物体只遍历一次,顶点着色阶段按左眼和右眼的矩阵分别做透视变换,把所有顶点打包进一个实例列表。移动端头显和独立式头显对这一点尤其敏感,因为CPU性能弱,批次提交数量少了,发热和掉帧压力都会小一截。
但这不等于说物体被处理一次就完全免费。像素着色阶段仍然要覆盖两个视口,填充率负担没有减半,依旧两倍于单眼渲染。所以要配合另一个层面的优化:遮挡剔除和可见性裁剪。虽然双眼视锥大部分重叠,剔除逻辑不能用左眼判断后就盲把结果用到右眼,在靠近视锥边缘的位置,一个物体可能只在右眼中可见。实践中会做一定程度的保守外扩,宁可多画一点,不能漏帧。
4.2 注意力就是预算:注视点渲染凭什么能省这么多
延迟和画质之间还有一条更激进的路:利用人类视网膜本身的特点。视网膜中央凹分辨率最高,周边视力急剧下降。头显里即使屏幕做到4K,人眼真正能分辨的细节也只占中心一小块。如果头显带眼动追踪,渲染管线就可以拿注视点作为中心,在周边区域降低像素密度或不均匀着色,把省下来的GPU预算用在刀刃上。这个技术叫注视点渲染,配合可变速率着色,在高端头显上已经成为标配之一。
为什么这一点对实时虚拟现实特别关键?因为延迟控制的本质是卡死帧预算。你把每帧的GPU时间从12ms压到8ms,等于给预测算法省出4ms外推余量,也意味着帧率不足时异步时间扭曲能少介入一些。分辨率提升带来的收益在快速转头时会完全被模糊掉,而注视点渲染相当于把分辨率投入到了用户真正看得清楚的地方。在VR这种视野覆盖范围大、张数多的场景里,均匀渲染本身就是一种浪费。
所以课堂上老师布置了一个很实际的原则:先保证端到端延迟达标,再谈像素分辨率;先看中央凹区域画质,再考虑周围做低分辨率;先花时间做剔除和单通道立体,再考虑加后处理特效。渲染优化的目的,始终是给“时间一致性”留出空间。
5. 我自己的小实验:把理论换算成数字
笔记里光有公式和图表没有实感。课后我搭了一个模拟项目X,场景很简单:一个空房间,相机按固定角速度旋转,脚本记录四段时间戳——姿态更新事件发生时间、应用逻辑拿到最新姿态的时间、渲染线程提交画面的时间、画面进入显示队列的时间。然后把扫描输出和像素响应估算加进去,就能得到端到端延迟近似值。
流程大致是这样:
- 在运动更新函数里记录当前事件触发时刻 t0。
- 在游戏逻辑循环中记录姿态被读取的时刻 t1。
- 在渲染线程完成命令提交后记录 t2。
- 使用引擎的帧回调钩子,在Present前记录 t3。
- 最后把显示器的扫描偏移和像素响应时间按固定值估算,算出总延迟。
这种测量方式肯定不如高速相机精准,但足够暴露各环节耗时和队列问题。模拟结果很有参考价值:
| 刷新率 | 近似的端到端延迟 | 以90°/s转头时的角度误差 |
|---|---|---|
| 60Hz | 35~45ms | 3.2°~4.1° |
| 90Hz | 22~30ms | 2.0°~2.7° |
| 120Hz | 16~22ms | 1.4°~2.0° |
5.1 一次简单的软件侧延迟模拟
表格里最扎眼的一组数字是角度误差。头部以每秒90度的速度转动,20ms延迟对应的视角偏差大约是1.8度,看起来不大,但在视野中心已经有明显滞后感。如果转动速度提高到每秒200度——真实卷头动作很容易达到——同样延迟下的偏差会涨到4度。这就是为什么游戏里的镜头旋转不需要高刷新率,而VR里转头那一瞬间最容易露馅。
为了排除偶然性,我分别在固定帧率控制下压测,并手动限制渲染队列深度。发现一个重要现象:平均帧率几乎一样时,队列深度为2会让延迟数字比队列深度为1多出大约8到12ms。这个差距恰好落在用户感知阈值附近。所以优化VR性能时,强制应用占满预渲染帧是性能大敌,必须在引擎环境里把最大预渲染帧数设成1。
5.2 实测中让我印象最深的一件事
另一个发现是“掉帧并不可怕,可怕的是掉帧后回到正常帧率的过程”。模拟中偶发一个长帧,接下来一帧往往会出现补偿性的短帧,因为显示端仍然按恒定刷新周期取帧。这种情况下,呈现给用户的画面其实发生了两次时间跳跃:一次等待,一次追赶。帧率统计表上只是平均掉了几个ms,但姿态与画面的错位感非常明显。
真正能缓解这个问题的,反而不是提升性能,而是让渲染结果在低负载时也不要提前完成太多。工程上的做法是让GPU渲染完成时机尽量贴近垂直同步到来,或者使用异步时间扭曲把最后一小段姿态变化修正掉。我后来在自己的项目里,一律要求先看两样东西:帧完成时间时间线,以及姿态数据跨渲染线程的同步次数。平均60帧一秒只是最低门槛,不是健康指标。
6. 笔记之外:一些值得记在脑子里的经验
课讲完,老师抛出一个问题:如果预测和时间扭曲都已经做得很好,还有哪些地方会让用户觉得“不实时”?大家讨论了一圈,答案集中在两方面:一是手部控制器和虚拟手的同步,二是强交互场景中物理反馈的时序。这两个问题都和头部姿态追踪无关,但同样遵循“运动到光子延迟”的分析框架,只是把“光子”换成了“反馈动作”。
我给自己的总结是三条经验。
第一条,实时VR的复杂度可以简化为“姿态信息在管线里流动的速度”。不要只盯着画面质量,先画出你的数据从传感器到显示设备的路径图,标出每一段的耗时,你会在图上找到所有隐藏的队列和缓冲。
第二条,预测、时间扭曲、补帧都是缓解品,不是主角。它们能提高平均体验,掩盖帧率和延迟的波动,但无法取代一个稳定的渲染帧预算。如果你发现大量帧都要靠扭曲来兜,说明应用本身的负载已经超出硬件承受范围,该考虑降渲染质量,而不是继续加补帧。
第三条,调试时少看平均值,多看时间线。平均帧率、平均延迟这些数字会把长尾问题抹掉。实时VR的用户感受到的,恰恰是长尾里那几次延迟尖峰。
下次课要进入交互状态预测和手部追踪的理论,我打算把这次测量端到端延迟的方法带过去,看看能不能同时量出手部反馈的延迟边界。到时候如果整理出有价值的内容,再写成笔记七。