
如果你戴上头显转头发现画面慢了那么一丁点才会意识到“实时”这个词在虚拟现实里有多苛刻。实时虚拟现实技术理论这门课讲到这里已经从前五次的视觉感知、交互基础推进到第六次真正硬核的话题渲染与延迟之间的关系。这次的课堂笔记核心是一条经常被提起但很少被讲透的链路——运动到光子延迟再配合姿态预测、异步时间扭曲、双眼渲染优化这些配套手段。对正在做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应用逻辑5msCPU提交3msGPU渲染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转头时的角度误差60Hz35~45ms3.2°~4.1°90Hz22~30ms2.0°~2.7°120Hz16~22ms1.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的用户感受到的恰恰是长尾里那几次延迟尖峰。下次课要进入交互状态预测和手部追踪的理论我打算把这次测量端到端延迟的方法带过去看看能不能同时量出手部反馈的延迟边界。到时候如果整理出有价值的内容再写成笔记七。