Chapter 141: VR Rendering Models
VR 渲染模型要解决的主问题,是把同一个虚拟世界在同一帧内稳定呈现给左右眼,并让图像跟随头部运动保持足够低的感知延迟。普通实时渲染通常围绕一台相机、一个 back buffer 和一次呈现节奏组织;VR 帧需要围绕 runtime、预测姿态、双眼视图、per-eye render target、畸变校正和帧截止时间组织。
读完本章后,读者应能追踪一帧 VR 图像从 pose query 到 submit frame 的完整路径,判断一个 VR 渲染问题属于视图错误、资源布局错误、畸变处理错误、帧时序错误,还是性能预算失控。本章使用一帧“用户向右转头时画面发生拖影和轻微眩晕”的 VR 场景作为贯穿材料:场景本身很简单,左右眼各渲染一个带地面网格、近处立方体和远处墙面的房间;问题集中在这一帧的姿态、双眼投影、提交时机和帧稳定性。
本章采用 OpenXR 风格的抽象对象描述 VR 管线。OpenXR 把应用和 XR runtime 之间的关系定义为接口:应用创建实例和会话,查询视图与预测姿态,渲染到 swapchain,再把 composition layer 交给 runtime 合成。Khronos 的 OpenXR 1.1 specification 明确把 view configurations、swapchain image management、view and projection state、frame synchronization、frame submission 和 compositing 放在渲染路径中,本章借用这条结构讲解工程判断。具体平台仍会在显示刷新、异步重投影、畸变网格、手部跟踪、眼动追踪和电源管理上存在差异。
本章的结论可以先给出:VR 渲染的稳定性来自三个闭环。第一个闭环是空间闭环,左右眼视图必须来自同一个预测显示时间下的 head pose、eye pose 和 projection。第二个闭环是时间闭环,CPU、GPU、runtime compositor 与显示扫描需要围绕 frame deadline 对齐。第三个闭环是质量闭环,渲染分辨率、MSAA、G-buffer、透明物体、foveated rendering 和 dynamic resolution 要共同服务稳定帧率。
141.1 VR 渲染的基本需求与挑战
VR 渲染的输入首先是头显系统给出的空间状态,而输出是能被 runtime 合成到头显显示器上的一组图像层。这里的 runtime 指负责设备会话、跟踪数据、swapchain、帧同步和最终合成的软件层。应用负责生成内容图像;runtime 负责把图像放入正确的显示时序和设备合成路径。这个分工决定了 VR 渲染的工程重点:应用需要在正确时间为正确视图生成图像,并在 deadline 前提交。
贯穿材料中的房间场景只有一个立方体和几面墙。单屏渲染时,相机矩阵稍晚更新通常表现为普通摄像机延迟;VR 中,用户头部已经转向右侧,画面却仍按较旧姿态显示,眼睛接收到的视觉运动和前庭系统感知到的头动发生偏差,拖影、漂移和眩晕风险都会上升。这里的视觉现象给出第一条判断:VR 画面错误要同时看空间一致性和时间一致性。
双眼视图是 VR 的第一项硬约束。左眼和右眼不是把同一张图像平移一下,它们各自有 eye pose、view matrix、projection matrix 和 render viewport。IPD(interpupillary distance,瞳距)决定左右眼原点之间的物理间距;每只眼的 projection 还要匹配镜片、屏幕位置和 runtime 给出的推荐视场。立方体如果在近处,左右眼看到的相对位置差异会更明显;墙面如果在远处,左右眼差异会更小。深度感正是来自这种可预测的视差。
高刷新率是第二项硬约束。VR 头显通常以 72Hz、80Hz、90Hz、120Hz 或更高节奏刷新,具体取决于设备、模式和 runtime。帧预算可以粗略写成 1000ms / refreshRate:90Hz 下每帧约 11.11ms,120Hz 下每帧约 8.33ms。这个预算覆盖 CPU scene update、command recording、GPU render、runtime compositor、畸变和显示扫描的相关路径。应用看到的 GPU pass 时间小于预算,只说明内容渲染部分暂时可接受;最终体验还要看提交时序和 compositor 余量。
低延迟是第三项硬约束。motion-to-photon 指头部运动发生到对应光子从显示器发出的时间跨度。应用无法直接控制整条链路,但可以控制几项关键动作:使用 runtime 给出的 predicted display time 查询 pose,在尽量靠近渲染开始的位置更新 view 常量,在 GPU 工作量上保留 compositor 余量,并让动态分辨率、LOD 和特效降级响应帧预算。贯穿材料中的右转头拖影,常见来源就是 pose query 太早、CPU 队列堆积太深、GPU pass 超时或 compositor 被迫复用旧帧。
畸变校正是 VR 与普通屏幕渲染的另一处分界。头显镜片会改变光线路径,应用渲染出的线性图像需要经过 lens distortion compensation 才能在用户眼中接近正确几何。现代 runtime 通常持有设备标定数据并执行最终畸变合成,应用更常见的职责是渲染到 runtime 提供的 swapchain,并遵守推荐分辨率、视场和 layer 格式。应用自行做畸变时,需要持有设备镜片参数和采样策略;跨设备引擎通常把这部分交给 runtime。
VR 眩晕风险来自多个因素叠加。空间层面,左右眼矩阵不一致、IPD 错误、near plane 过近、scale 与现实感知冲突,会破坏深度和身体预期。时间层面,pose 与显示时间错位、帧间隔抖动、dropped frame、reprojection 频繁介入,会让运动反馈不稳定。内容层面,强制相机加速度、固定屏幕 UI、缺少地面参照、快速旋转,会放大不适。渲染模型需要把这些现象放回 frame、view、resource 和 deadline 中分析。
下面的图把一帧 VR 渲染放在 runtime、应用和 GPU 之间。图的边界是渲染帧路径;输入手柄、音频、空间锚点和网络同步暂时不展开。
这条路径的关键点在于反馈环。用户看见图像后继续移动头部,runtime 在下一帧给出新的预测姿态,应用再生成新的左右眼图像。任何一环推迟都会把后续环节压缩。工程排查时应先确认一帧使用的 predicted display time,再确认左右眼 view/projection 来自同一时间点,然后检查 GPU pass 是否在 deadline 前完成。
141.2 Stereo Rendering 技术原理
Stereo Rendering 的工作定义是:同一个世界状态在同一预测显示时间下,分别从左眼和右眼视点生成图像,再由 runtime 合成为头显可显示的双眼输出。它解决的不是“画两次”的简单重复,而是空间、投影、资源和 shader 常量之间的绑定一致性问题。左右眼任何一个矩阵、viewport、depth range 或 resource binding 错位,都会在近距离几何上被迅速放大。
eye pose 表示每只眼相对某个 reference space 的姿态。reference space 是 runtime 和应用约定的空间基准,例如本地空间、舞台空间或坐姿空间。OpenXR 规范使用右手坐标系,距离单位在物理距离语境中使用米;这让应用能把真实尺度与虚拟尺度对齐。房间场景中,立方体边长如果设置为 1.0,读者应把它理解成 1 米级别的对象,而不是任意屏幕单位。
每只眼的 view matrix 通常来自 eye pose 的逆变换。世界中的点先从 world space 进入当前眼睛的 view space,再乘以当前眼睛的 projection matrix 进入 clip space。左右眼 projection 常常是非对称 frustum,因为镜片和显示区域相对眼睛的位置并非理想对称。工程上不应手写一个左右完全对称的投影矩阵去替代 runtime 提供的 FOV;稳定做法是使用 runtime 返回的每眼 FOV 或 projection 参数生成矩阵。
IPD 影响两眼 view origin 的间距。IPD 过小会让世界显得更大,IPD 过大会让世界显得更小;更严重的是,左右眼视差和用户真实身体感知不匹配。对于引擎层代码,IPD 通常不直接由应用硬编码,而是由 runtime 根据用户设置和设备标定参与 eye pose 计算。应用拿到的是每只眼的 pose 与 projection,核心任务是保持同一帧内这些数据配套使用。
per-eye render target 有几种常见布局。第一种是两张独立 texture,左眼一张、右眼一张,逻辑清楚,资源切换较直接。第二种是 texture array,左眼和右眼使用不同 array layer,适合 single-pass stereo。第三种是单张大 texture 中左右分区,viewport 和 scissor 需要严格设置。不同 API 和 runtime 对推荐格式、swapchain image 数量、MSAA resolve 和 layer 提交有不同接口细节;可迁移的判断维度是图像归属、viewport、depth、resolve 和提交描述是否一致。
single-pass stereo 的目标是减少 CPU 提交和部分 GPU 重复工作。典型做法是把一份 draw call 扩展到两个 view,shader 通过 view index 选择每眼矩阵,输出写入 texture array 的不同 layer。这个模型适合场景相同、材质相同、左右眼只在 view/projection 上变化的内容。它的边界也很明确:shader 中任何基于屏幕坐标、采样历史、随机噪声、clip plane 或后处理 UV 的逻辑,都要确认 view index 参与计算,保证左右眼结果分离。
下面的简化 C++ 片段展示 stereo 常量的最小组织方式。它不是完整 OpenXR 程序,只表达同一帧中左右眼矩阵与 viewport 的绑定关系。
struct EyeFrameData {
Matrix4 view;
Matrix4 projection;
Viewport viewport;
uint32_t targetLayer;
};
struct StereoFrameData {
Time predictedDisplayTime;
EyeFrameData eyes[2];
};
void RenderStereoFrame(const StereoFrameData& frame) {
for (uint32_t eye = 0; eye != 2; ++eye) {
BindRenderLayer(frame.eyes[eye].targetLayer);
SetViewport(frame.eyes[eye].viewport);
SetViewProjection(frame.eyes[eye].view, frame.eyes[eye].projection);
DrawVisibleScene();
}
}
这段代码证明的点是:stereo 数据应围绕 frame 聚合,而不应分散在相机、材质、后处理和提交代码中各自临时读取。predictedDisplayTime 是这一帧空间数据的时间标签;两只眼的 view/projection 与它配套。排查左右眼错位时,先检查同一帧内这组数据是否被同时更新,再检查渲染目标 layer 和 viewport 是否指向正确眼睛。
立方体场景可以用来验证 stereo 正确性。把一个小立方体放在用户正前方 1 米处,再放一面网格墙在 10 米处。左右眼看到近处立方体相对远处墙面的偏移应明显大于远处对象之间的偏移。如果两眼图像几乎完全相同,eye pose 可能没有生效;如果近处物体深度反向,左右眼资源可能交换;如果一只眼边缘拉伸,projection 或 viewport 可能与 runtime 推荐视图不一致。
141.3 实现 VR 渲染基础流程
VR 渲染基础流程可以分成六段:接入 runtime、创建会话与 swapchain、等待帧、查询预测姿态与 views、渲染 per-eye 图像、提交 composition layer。OpenXR 的程序员视角也采用类似结构:创建 instance 连接 runtime,选择 system,创建用于渲染 view 的 buffer,再创建 session 并进入 XR rendering loop。实际 API 名称会随 OpenXR、平台 SDK 或引擎封装变化,但工程路径基本稳定。
第一段是 runtime 接入。应用需要查询可用 runtime、扩展、图形 API 绑定能力和 view configuration。OpenXR 的扩展机制要求应用先查询可用扩展,再在创建 instance 时启用需要的扩展;这说明 VR 引擎不能把 foveation、eye tracking、composition layer depth 或平台性能指标当成永久可用能力。稳定做法是把能力查询集中到 XR backend 的初始化阶段,并为核心 stereo rendering 保留基础路径。
第二段是 swapchain 建立。swapchain 是应用和 runtime 之间交换图像所有权的资源集合。应用从 swapchain acquire image,等待图像可写,渲染完成后 release image;runtime 在合成阶段读取对应图像。这里的关键不是“拿到 texture 就能画”,而是图像生命周期要匹配 runtime 的所有权规则。Vulkan、D3D、Metal、OpenGL 的底层资源状态和同步方式不同,XR backend 应把 acquire、wait、render、release、submit 串成固定状态机。
第三段是 frame synchronization。应用通常调用等待帧接口取得下一帧的 frame state,其中包含 runtime 预测的显示时间和当前是否应渲染的状态。这个时间不是普通 CPU 当前时间,它是用来查询这一帧视图姿态的时间基准。房间场景中的右转头拖影,如果应用在上一帧 update 阶段就读取头部姿态,然后经过很长 CPU 队列才渲染,最终图像就会落后于真实头动。正确路径是围绕 predicted display time 查询视图,并让 GPU command 尽量贴近这一帧数据。
第四段是 pose query 与 per-eye render。应用使用 predicted display time 在 reference space 中定位 views,得到左右眼 pose 和 FOV,再构造 view/projection 常量。随后应用为每只眼设置 render target、viewport、depth buffer、MSAA buffer 和场景常量。对于 texture array single-pass stereo,应用还要设置 view mask 或 view index 路径。这里最常见的错误是把上一帧的 view 常量留在某个 uniform buffer 中,或者后处理 pass 使用了单眼屏幕 UV。
第五段是畸变与合成。多数现代 runtime 在应用提交 layer 后执行最终畸变、时间重投影和合成。应用提交的通常是 projection layer:每只眼包含 swapchain image、sub-image 区域、pose 和 FOV。runtime 再根据设备标定、最新头部姿态和显示时序把 layer 映射到头显显示。应用层代码应保证 layer 描述与实际渲染内容一致:左眼图像配左眼 pose,右眼图像配右眼 pose,图像区域配实际 viewport。
第六段是错误处理与状态恢复。头显可能暂停会话、失去焦点、切换刷新率、改变推荐分辨率,或在移动设备上进入热限制状态。VR 渲染循环需要响应 session state、swapchain resize、visibility、shouldRender 和 runtime event。应用在不可见或不应渲染时继续提交重负载 GPU work,会增加功耗和队列压力;应用在恢复时沿用旧 swapchain 尺寸,也会造成拉伸或提交失败。
下面的流程图把基础循环按状态组织。图中省略平台窗口创建和图形 API 细节,只保留与 VR frame 有关的动作。
这个循环给出可复用的排查顺序。画面几何错位先查 Pose 和 Render 之间的数据绑定;某只眼黑屏先查 Swap、Render、Release 的图像所有权;画面在快速头动时漂移先查 Wait、Pose、Submit 的时间一致性;恢复头显后画面拉伸先查 Events 之后 swapchain 和 recommended size 是否重建。
一个最小 VR renderer 的伪代码可以写成下面这样。它表达的是控制流,不绑定某个 SDK 的真实函数名。
while (sessionRunning) {
PollRuntimeEvents();
FrameState frame = WaitFrame();
BeginFrame();
if (frame.shouldRender) {
View views[2] = LocateStereoViews(frame.predictedDisplayTime, appSpace);
SwapchainImage image = AcquireAndWaitColorImage();
BeginRendering(image);
RenderEye(0, views[0], image);
RenderEye(1, views[1], image);
EndRendering(image);
ReleaseColorImage(image);
SubmitProjectionLayer(frame.predictedDisplayTime, views, image);
}
EndFrame();
}
这段伪代码的关键点在于顺序:等待帧产生时间标签,定位 views 使用同一时间标签,渲染写入 runtime 管理的图像,提交 layer 说明图像与眼睛的关系。真实 OpenXR 循环中 xrWaitFrame、xrBeginFrame 和 xrEndFrame 具有同步语义;真实引擎中还会有资源上传、遮挡剔除、动画、物理和 UI。但只要这条主线稳定,复杂系统也能被拆回同样的 frame 问题。
141.4 延迟渲染在 VR 中的适配策略
延迟渲染在普通屏幕上的优势来自 G-buffer:几何 pass 先把位置、法线、材质、深度等信息写入多个 render target,lighting pass 再按屏幕像素计算光照。VR 中,这个模型会直接放大带宽和显存压力,因为左右眼都需要 G-buffer,分辨率也通常高于普通窗口。工程判断要从每帧写入量开始:每增加一个 G-buffer attachment,都要乘以每眼像素数、MSAA 样本数和刷新率。
G-buffer 带宽是 VR deferred shading 的第一类成本。假设单眼渲染分辨率为 1832 x 1920,双眼合计约 703 万像素。若 G-buffer 包含 albedo、normal、material、velocity、depth 等多个 attachment,90Hz 下每秒写入量会快速上升。tile-based GPU 上,多 render target 还会影响 tile memory 和 store 行为;immediate-mode GPU 上,显存带宽和 cache 压力更明显。由此得到的策略是压缩 G-buffer 格式、减少 attachment 数量、把可重建信息留到 lighting pass 计算,并把不参与主要光照的对象放入 forward path。
MSAA 是第二类成本。VR 对边缘闪烁敏感,MSAA 可以提升几何边缘质量;但 deferred shading 与 MSAA 结合会增加 G-buffer 样本存储和 resolve 成本。常见适配策略是 geometry pass 使用 MSAA depth/color 保存关键边缘,lighting pass 尽量按像素聚合,透明物体和高光细节走 forward 或 forward-plus。对于移动头显,MSAA sample count 经常需要跟动态分辨率和热状态联动。
透明物体是 deferred VR 管线中的第三类边界。G-buffer 适合不透明表面,透明玻璃、粒子、体积雾、光束和 UI 通常需要在 lighting 后按深度进行 forward composition。VR 中透明错误会更明显,因为左右眼透视差异会暴露排序问题和深度不一致。稳定管线通常把不透明 deferred pass、lighting、透明 forward pass、XR overlay 或 composition layer 分开组织,并为每一段保留明确的 depth 输入。
late stage reprojection 是 runtime 用来降低感知延迟的重要机制。它的工作思路是:应用提交的图像可能基于稍早姿态,runtime 在最终显示前使用更新的头部姿态对图像做旋转或空间重投影,从而减少头动延迟。这个机制能缓解小范围姿态误差,但无法恢复应用没有渲染出的新可见区域,也无法修复 shader 内容、阴影、反射和遮挡关系的全部变化。工程上应把它视作安全余量,而不是渲染管线长期超预算的解决方案。
foveated rendering 与 deferred shading 的关系需要按资源路径判断。固定注视点或眼动追踪的 foveated rendering 会降低周边区域 shading rate 或分辨率。对于 deferred 管线,降低几何 pass 分辨率可能影响深度、法线和边缘稳定性;降低 lighting pass shading rate 更容易保留几何轮廓。具体取舍取决于 runtime 支持、VRS(variable rate shading,可变速率着色)能力、眼动延迟和后处理路径。下一章会专门展开 foveated rendering,本章只把它作为 VR 延迟管线的适配入口。
VR 中的 deferred path 常见于 PC 头显、复杂光照场景和可控硬件环境。独立移动头显更常选择 forward、forward-plus、clustered forward 或混合管线,因为这些路径更容易控制带宽、MSAA 和透明物体。这个判断不表示 deferred 在 VR 中失效;它表示 deferred 的收益要和双眼分辨率、刷新率、G-buffer 写入量、后处理和 runtime compositor 余量一起评估。
下表给出 deferred VR 管线的检查维度。它不是性能结论表,而是排查时的证据组织方式。
| 问题对象 | 主要证据 | 常见调整 |
|---|---|---|
| G-buffer 带宽 | 每眼分辨率、attachment 数量、格式、store 行为 | 压缩格式,合并材质通道,减少 velocity 或 position attachment |
| MSAA 成本 | sample count、resolve 时间、边缘闪烁位置 | 降低 sample count,使用 TAA 或几何边缘专项处理 |
| 透明路径 | 粒子、玻璃、UI 的左右眼排序和深度关系 | 透明 forward pass,独立 depth 输入,减少屏幕空间假设 |
| 重投影压力 | dropped frame、reprojection ratio、头动拖影 | 降低 GPU pass 时间,减少 CPU 队列深度,调整 dynamic resolution |
| 周边质量 | foveated 区域边界、眼动延迟、周边闪烁 | 平滑 rate map,限制突变,保留中心高质量 |
贯穿材料中的房间场景可以扩展一个 deferred 测试:给墙面增加法线贴图和四个动态点光源,把近处立方体设置为高对比边缘。若普通视角正常,VR 中快速转头时边缘闪烁并伴随 dropped frame,应优先检查 G-buffer 写入和 lighting pass 是否压缩了 GPU 预算,再检查 runtime compositor 是否频繁重投影。若只有透明 UI 漂移,应检查 UI 是否以单眼屏幕空间后处理方式合成。
141.5 性能与帧稳定性考虑
VR 性能评估的核心指标不是平均帧率,而是每一帧是否在 deadline 前稳定交付。frame deadline 是 runtime 和显示时序给应用留下的提交边界;应用错过 deadline 后,runtime 可能复用旧帧、触发重投影、插入合成帧或降低体验质量。平均 90 FPS 无法证明体验稳定,因为少量 25ms 帧就能被用户感知为卡顿或拖影。
dropped frame 是第一类直接信号。它表示应用或系统在某个显示周期没有提供可用新帧,runtime 需要用旧图像或合成策略填补。排查 dropped frame 时要先分 CPU 和 GPU:CPU 侧看 simulation、culling、command recording、driver submit 和 XR API wait/submit 的时间;GPU 侧看 shadow、G-buffer、lighting、post-process、MSAA resolve、copy 和 queue wait。只看 draw call 数量无法定位 VR 卡顿,pass 时间和提交时序才是核心证据。
motion-to-photon 是第二类体验信号。应用可观察到的 proxy 指标包括 pose age、CPU queue depth、GPU frame time、compositor timing 和 runtime 提供的 performance metrics。不同平台暴露的名称不同;OpenXR extension、厂商 SDK、SteamVR、Meta、WMR、Apple 或移动平台工具会给出不同入口。可迁移的方法是把指标归到三段:采样到提交、提交到合成、合成到显示。房间场景的右转头拖影如果发生在 GPU 未超时的情况下,就要重点检查 pose age 和队列深度。
thermal 是独立头显和移动 XR 的长期约束。头显内部空间有限,GPU、CPU、显示、传感器和无线网络共享功耗与散热预算。短时间达标的帧率在几分钟后可能因 thermal throttling 下降。工程策略应把 dynamic resolution、LOD、阴影级别、粒子数量、后处理质量和刷新率模式接入同一个质量控制器。控制器的输入是 frame time、thermal level、battery state 和 runtime 建议;输出是可以逐级下降的渲染开销。
dynamic resolution 是 VR 中常用的稳定手段。它通过调节渲染分辨率或 shading rate,把 GPU pass time 拉回预算。调节策略需要设置上限、下限、响应速度和恢复速度。响应太慢会连续错过 deadline;响应太快会造成清晰度跳变和用户可见闪烁。对于 stereo rendering,左右眼分辨率应作为同一质量等级处理,特殊 foveated path 也要保证中心区域稳定。
帧稳定性需要同时管理 CPU 队列和 GPU 队列。普通引擎为了吞吐可能允许多帧 in flight,VR 中过深队列会增加 pose 到显示的时间。更合适的策略是控制 frame pacing:CPU 提前量有限,GPU 工作量可预测,runtime compositor 保留余量。应用还要识别 shouldRender、visibility 和 session focus,减少后台无效渲染,把功耗留给可见帧。
下面给出一套 VR 帧问题排查顺序。这组步骤适合用于房间场景,也适合迁移到复杂引擎。
- 先记录刷新率、目标帧预算、每眼推荐分辨率和 runtime 模式,确定当前 deadline 条件。
- 再记录 CPU frame time、GPU pass time、compositor timing、dropped frame 和 reprojection 比例,区分瓶颈位置。
- 接着检查 predicted display time、pose query 时间点、view/projection 更新位置和 CPU 队列深度,确认空间数据没有滞后。
- 然后按 pass 拆 GPU:shadow、depth、G-buffer、lighting、transparent、post-process、resolve、copy,找出超预算段。
- 最后应用 dynamic resolution、LOD、MSAA、后处理和特效降级,并观察清晰度变化和帧稳定性是否同时改善。
这套顺序的关键是先看时序,再看内容。只优化 shader 可能让某个 pass 变快,却仍然留下 pose age 过大的问题;只调 dynamic resolution 可能降低 GPU 时间,却没有解决左右眼矩阵错位。VR 渲染模型把这些问题统一到一帧:这一帧使用什么时间的姿态,渲染到哪些资源,赶上哪个 deadline,runtime 如何合成,用户最终看到什么。
最小自检任务
给定一个 VR 房间测试场景:用户正前方 1 米处有一个立方体,10 米处有一面网格墙。应用以 90Hz 运行,目标帧预算约 11.11ms。最近出现三个现象:快速向右转头时画面有拖影;近处立方体在左右眼中的深度感偏弱;打开 deferred lighting 后偶发 dropped frame。请按本章的 VR 渲染模型,给出排查顺序,并说明每一步应观察的证据。
答案要点
先定位时序。90Hz 的单帧预算约 11.11ms,拖影需要先查 predicted display time、pose query 时间点、CPU queue depth、GPU frame time 和 compositor timing。若 GPU 未超预算但拖影明显,优先怀疑姿态采样过早、CPU in-flight 过深或提交时机错位。若 compositor 显示 dropped frame 或 reprojection 比例上升,应继续拆 CPU 和 GPU 时间。
再定位 stereo 空间数据。近处 1 米立方体应比 10 米墙面产生更明显左右眼视差。深度感偏弱时,检查左右眼 eye pose 是否来自同一预测显示时间,IPD 是否由 runtime 参与计算,view/projection 是否分别绑定到左眼和右眼,per-eye render target、viewport、array layer 和提交 layer 是否匹配。若两眼图像几乎相同,eye pose 可能没有进入 shader 常量;若深度方向异常,左右眼图像或 pose 可能交换。
接着定位 deferred 管线成本。打开 deferred lighting 后出现 dropped frame,应按 shadow、G-buffer、lighting、transparent、post-process、MSAA resolve 和 copy 拆 GPU pass time。重点观察 G-buffer attachment 数量、格式、每眼分辨率、MSAA sample count 和 store 行为。若 G-buffer 写入或 resolve 占用预算过高,可以压缩格式、减少 attachment、降低 MSAA 或把透明和部分材质移入 forward path。
最后验证稳定策略。把 dynamic resolution、LOD、阴影质量、后处理和特效降级接入同一质量控制器,观察 dropped frame、reprojection 比例、中心清晰度和周边闪烁是否同时改善。结论应同时覆盖空间闭环、时间闭环和质量闭环:正确的 VR 帧使用同一预测时间下的左右眼视图,按 runtime swapchain 生命周期提交,在 deadline 前给 compositor 留出余量。
本章知识点总结
- VR 帧模型:VR 渲染围绕 runtime、预测姿态、双眼视图、per-eye render target、畸变合成和 frame deadline 组织。
- 空间闭环:左右眼 view/projection 必须来自同一预测显示时间下的 head pose、eye pose 和 runtime 视场数据。
- 时间闭环:motion-to-photon 受 pose query、CPU 队列、GPU pass、runtime compositor 和显示扫描共同影响。
- 双眼视差:近处物体应产生更明显左右眼差异,视差异常通常指向 eye pose、IPD、projection 或眼图资源绑定问题。
- 资源布局:per-eye render target 可以使用独立 texture、texture array 或单图分区,排查时要同时检查 viewport、layer、depth 和提交描述。
- 基础循环:VR 渲染循环按 runtime 事件、wait frame、locate views、per-eye render、release image 和 submit layer 推进。
- swapchain 生命周期:应用需要遵守 acquire、wait、render、release、submit 的图像所有权顺序,具体同步细节由图形 API 和 runtime 决定。
- 畸变合成:现代 runtime 通常负责最终畸变、重投影和 layer 合成,应用应保证提交图像与每眼 pose、FOV 和 sub-image 匹配。
- Deferred 成本:延迟渲染在 VR 中会放大 G-buffer 带宽、MSAA 存储、resolve 和透明合成压力。
- 重投影边界:late stage reprojection 能减少小范围姿态误差的感知延迟,但无法补回未渲染的新可见区域和完整遮挡变化。
- 帧稳定性:VR 性能评估应优先看 frame deadline、dropped frame、reprojection 比例、GPU pass time 和 compositor timing。
- 动态质量:dynamic resolution、LOD、MSAA、阴影、后处理和特效降级应服务稳定帧率,并控制清晰度突变。
- 排查顺序:VR 问题先查时序和 pose,再查左右眼数据绑定,接着拆 GPU pass,最后验证质量控制策略。