Chapter 144: Stereo and Multi-View Rendering
立体渲染把同一个世界从多个观察位置生成多张图像,再交给头显、AR compositor、多屏系统或光场显示设备合成成一个沉浸式结果。读完本章后,读者应能定位一个多视图 frame 中的 view 来源,判断每个 view 使用哪组矩阵、目标纹理和资源状态,追踪视差错误来自相机模型、投影矩阵、资源绑定还是提交布局。
本章以一个典型 XR frame 作为贯穿材料:运行时给出左眼和右眼的预测姿态与视场角,应用为每个 view 构造 view matrix 和 projection matrix,把场景渲染到 texture array 的不同 layer,最后把这些 layer 作为 projection layer 提交给运行时 compositor。这个材料覆盖双眼 VR,也能扩展到 AR passthrough、多屏 CAVE 和光场显示。
多视图渲染的核心判断是:视图数量增加后,世界数据和材质数据应尽量保持共享,随 view 变化的数据集中在 eye pose、projection、裁剪范围、render target layer 和少量 view-dependent pass 中。这个判断能把视觉结果、GPU 资源和 API 状态连接起来。左眼正确、右眼漂移,通常先查 per-view 常量和 render target layer;两眼都有错误,通常先查世界坐标、tracking space、projection 生成和提交时机。
图形 API 的多视图能力提供的是提交与执行上的组织方式。OpenXR 规范把 view configuration、view/projection state、swapchain 和 frame submission 放在同一条 frame 路径中说明;Vulkan 的 VK_KHR_multiview 说明 multiview 可以用一组命令以不同 view 行为执行,并通过 ViewIndex 这类内建量区分 view。正文使用这些公共模型建立工程判断,具体平台仍以目标 runtime、GPU 和图形后端的能力查询为准。可回溯材料包括 OpenXR 1.1 specification 与 Vulkan VK_KHR_multiview reference。
144.1 多视图渲染的基本模型
多视图渲染首先要把“相机”拆成 view array。每个 view 有自己的观察姿态、投影参数、视口或目标 layer;多个 view 共享场景图、网格、材质、光照输入和大部分 GPU pipeline state。对本章的 XR frame 来说,view array 通常包含 left 和 right 两个元素,每个元素对应一个 eye pose、一个 view matrix、一个 projection matrix 和一个 texture array layer。
OpenXR 路径中的关键输入是预测显示时间下的 view pose 和 field of view。运行时在 tracking space 中描述每个 view 的位置与朝向,应用把它转成渲染相机需要的 view matrix;运行时提供的 fov 用于构造每眼投影矩阵。OpenXR 中用于物理距离的空间量以米为单位,这一点会影响 IPD、near plane、远近物体深度和手部交互对象的尺度判断。
这个 frame 可以用下面的路径描述。图中每个节点都对应一个可检查对象:运行时输出 view 状态,应用生成矩阵,渲染器把 draw 写入不同 layer,最后 compositor 使用提交描述合成显示图像。
这条路径的工程意义是把错误定位点固定下来。view array 数量或顺序错误,会表现为左右眼交换、单眼黑屏或 projection layer 对应错误。矩阵错误会表现为双眼视差异常、场景尺度漂移或头动时图像相对世界滑动。layer 绑定错误会表现为两眼显示同一张图、右眼沿用左眼结果或每帧偶发闪烁。提交描述错误会让 compositor 按错误矩形、错误 fov 或错误 alpha 规则解释渲染结果。
一个简化伪代码可以表达多视图 frame 的组织方式。它展示的是数据关系,真实工程还需要补齐 swapchain acquire/release、同步、错误码处理和平台后端对象。
struct ViewState {
Matrix4 view;
Matrix4 projection;
uint32_t targetLayer;
};
std::array<ViewState, 2> buildStereoViews(const XrViews& xrViews) {
std::array<ViewState, 2> views{};
for (uint32_t viewIndex = 0; viewIndex < 2; ++viewIndex) {
views[viewIndex].view = inversePose(xrViews.pose[viewIndex]);
views[viewIndex].projection = projectionFromFov(xrViews.fov[viewIndex]);
views[viewIndex].targetLayer = viewIndex;
}
return views;
}
这段代码的检查重点是数组下标和语义对应。viewIndex 同时选择 pose、fov、projection、常量缓冲区元素和目标 layer。任何一个位置使用固定下标,都会让一只眼使用另一只眼的状态。立体图像对这种错误敏感,因为大脑会把左右眼差异解释成深度;错误差异会直接变成重影、压迫感或眩晕风险。
裁剪也要放进 view array 模型中判断。最保守做法是对每个 view 单独做 frustum culling,结果准确但 CPU 开销增加。常见优化是构造覆盖双眼的 union frustum,先得到共享可见集,再在 GPU 或后续 pass 中处理每眼细节。union frustum 会提交少量单眼不可见物体,但能减少两次场景遍历;per-eye culling 能减少 draw 数量,但会增加 CPU 侧状态分叉和可见性缓存复杂度。
本节建立的基本模型是:多视图 frame 由共享世界输入和 per-view 状态共同决定。工程检查顺序应先确认 view count 与 view order,再检查每个 view 的 pose、projection、target layer 和提交描述,最后比较共享可见集与 per-eye 可见集是否符合性能目标。
144.2 视差与深度感知的实现方式
视差是双眼看到同一物体时在图像平面上的位置差。对 XR 渲染来说,视差来自两个 eye pose 的空间基线,也来自每眼投影矩阵对同一世界点的不同投影。IPD 表示双眼瞳孔距离,工程中由 runtime、设备校准或用户配置提供;渲染器应把它体现在左右 eye pose 的平移差异中,并用对应 fov 生成每眼 projection。
在简化针孔相机模型中,某个物体的屏幕视差近似满足 disparity ≈ focalLength * IPD / depth。这里的 depth 是物体在相机前方的距离,focalLength 对应投影矩阵中的焦距缩放,IPD 对应左右眼观察点之间的物理基线。这个式子说明近处物体的左右眼位置差更大,远处物体的左右眼差异更小。视觉系统利用这种差异恢复深度,但它还会结合遮挡、透视缩放、运动视差、阴影、景深和已知物体尺寸。
XR 渲染中的 convergence 可以理解为用户双眼注视某个深度时的汇聚状态。实时头显应用通常把相机保持为左右平移关系,并使用每眼非对称投影矩阵覆盖对应 fov。这样做能让几何关系保持稳定。直接把左右相机向同一个点旋转的 toe-in 方法会引入垂直视差和投影面旋转差,在头显里容易放大不适感;它更适合受控离线双目相机模拟,实时 XR 工程中应把旋转差异交给 tracking pose 和 runtime fov,而把 convergence 作为观看者眼睛的生理结果处理。
深度感知还依赖显示校准。头显的屏幕、镜片、畸变校正、眼点位置和 IPD 校准共同决定最终图像如何进入双眼。应用侧通常把镜片畸变交给 runtime compositor 处理,自己的任务是渲染到 runtime 要求的 swapchain 图像,再让 compositor 执行镜片校正、时间重投影和显示合成。应用能控制场景尺度、near/far plane、projection 矩阵、深度提交和 view layer 内容。
当场景尺度出错时,深度感会整体失真。比如应用把 tracking space 的米误当成厘米,虚拟桌子会在双眼中产生过大的视差,用户会感觉物体贴脸或尺寸异常。反过来,世界单位过大时,近处物体的视差不足,场景像缩在远处的模型。排查时先放置已知尺寸的基准对象,例如 1 米立方体和 0.1 米手柄模型,再观察左右眼投影差异是否符合期望。
视差错误可以按可观察现象分组。两眼看到同一物体但水平差异过大,先查 IPD、world scale 和 projection 焦距。两眼出现垂直错位,先查相机旋转、view matrix 构造和 fov 上下边界。头动时物体相对现实世界滑动,先查 pose prediction、tracking space 和提交时机。单个透明层或 UI 双影严重,先查该层的深度、composition layer 类型和是否跟随头部空间。
本节的结论是:深度感来自 per-eye pose、projection 和显示校准的组合。应用调试时应把“视差大或小”转换成可检查量:IPD 是否进入 eye pose,projection 是否来自每眼 fov,世界单位是否以米为基础,near/far plane 是否压缩了深度精度,提交给 compositor 的深度与图像是否一致。
144.3 高效 Multi‑View 管线设计
高效 multi-view 管线的目标是把 view 差异收敛到最少的 GPU 状态和 shader 输入上。朴素方案会完整渲染左眼一次、右眼一次;它实现简单,但 CPU 提交、场景遍历、状态切换和部分 GPU 工作会翻倍。更好的组织方式是共享可见集、共享 pipeline、共享材质绑定,把 view 差异放入 per-view constants,并使用 view instancing、texture array 或 API multiview 能力一次性组织多 view 输出。
view instancing 的基本思路是让同一批几何为多个 view 执行。每个实例或每个 view 通过 viewIndex 读取自己的 view-projection matrix,然后输出到对应 render target layer。Vulkan multiview、OpenGL ES multiview、Direct3D view instancing、Metal 的 layered rendering 或类似能力在表达方式上不同,但工程目标一致:减少重复 command recording 和重复状态绑定,让 GPU 在单次管线组织中完成多个 view 的渲染。
下面的 GLSL 片段展示 per-view constants 的最小形态。gl_ViewIndex 选择当前 view 的矩阵,输入网格和材质保持共享,输出位置按当前 view 计算。
layout(set = 0, binding = 0) uniform ViewConstants {
mat4 viewProjection[2];
} viewData;
layout(location = 0) in vec3 inPosition;
void main() {
uint viewIndex = uint(gl_ViewIndex);
gl_Position = viewData.viewProjection[viewIndex] * vec4(inPosition, 1.0);
}
这段 shader 证明了 multi-view 的关键分界。顶点数据、索引缓冲、材质和 draw call 可以共享;位置变换中使用的 view-projection matrix 必须按 view 选择;输出目标由 API 的 multiview 或 layered rendering 规则决定。调试时如果两眼几何完全重合,优先检查 viewProjection 数组是否写入两组不同矩阵;如果右眼画面来自左眼 layer,优先检查 framebuffer、render pass view mask、texture array layer 或提交时的 sub-image 描述。
shared culling 应放在 CPU 与 GPU 之间协同设计。CPU 侧可以先使用覆盖所有 view 的宽 frustum 生成可见 draw 列表,GPU 侧再通过 per-view depth、per-view clip 或 shader 中的 view 条件细化。这个方案适合复杂场景和双眼视差较小的头显。多屏 CAVE 或光场显示的 view 分布更宽,union frustum 会迅速变大,过度提交的 draw 会增加;这类系统更适合分组 culling,按屏幕面或视角簇生成多个可见集。
per-view constants 的布局需要稳定。常见做法是用一个 uniform buffer、storage buffer 或 push constant 区域存储 view 矩阵、相机位置、视口参数和 depth range。所有 shader stage 使用同一套 viewIndex 规则读取数据。这样能减少 CPU 侧分支,也能让 RenderDoc、Xcode GPU tools 或 Nsight 这类工具在某个 draw 上清楚显示 view 数组和对应 layer。
multiview extension 带来的是组织收益,不自动消除所有重复成本。每个 view 仍需要不同 clip、不同 rasterization 覆盖、不同 depth 结果和不同 fragment 输出。场景受顶点处理、CPU 提交或状态绑定限制时,multiview 收益明显;场景受像素填充、透明 overdraw 或高成本 fragment shader 限制时,两眼像素仍然要分别计算,收益会受限。判断方式是拆开看 draw count、CPU command time、vertex invocations、fragment time 和 bandwidth。
本节建立的可复用设计顺序是:先确认目标平台支持的 view instancing 或 multiview 能力,再设计 texture array 与 view constants 布局,然后把 culling、draw submission、shader 访问和 frame submission 对齐到同一个 viewIndex 规则,最后用工具验证每个 draw 的 view 数量、layer 输出和 shader 常量。
144.4 纹理与资源共享优化策略
资源共享的原则是把与观察点无关的数据放在单份资源中,把与 view 相关的数据放在 array layer、per-view slice 或小型常量块中。材质贴图、网格缓冲、骨骼动画结果、光照参数、环境贴图和大部分 material buffer 通常可以跨 view 共享。color、depth、motion vector、velocity、部分 reflection 和 screen-space pass 通常需要 per-view 存储,因为它们依赖当前眼睛的投影和屏幕坐标。
depth 的“复用”要按层级理解。最终 depth buffer 的每个像素由当前 view 的投影决定,左右眼 depth 结果需要分别存放;depth pass 的 draw 列表、depth pipeline state、depth pyramid 构建代码和 texture array 管理可以共享。某些遮挡剔除可以使用双眼 union frustum 和保守 depth 结果做粗筛,再在每眼 depth 中完成精确测试。这样做把 CPU 遍历和资源调度共享起来,同时保留每眼正确的深度结果。
shadow map 通常更适合共享。阴影贴图从光源视角生成,与左眼或右眼相机位置的关系较弱;只要场景和光源在同一帧内稳定,双眼可以采样同一组 shadow atlas。例外出现在 view-dependent shadow、局部反射阴影或极近距离接触阴影中,这些 pass 可能依赖当前相机空间或屏幕空间,需要按 view 生成或补偿。
reflection 的共享边界更窄。环境 cubemap、probe、SSR 之前的粗反射数据可以共享;screen-space reflection 依赖当前 view 的 color、normal、depth 和屏幕射线,通常要为每个 view 计算。平面反射如果反射相机由当前 eye pose 推导,左右眼会得到不同反射视差;如果把平面反射当成远处近似,可以共享部分 reflection texture,但需要确认近处反射是否出现双眼深度冲突。
visibility buffer 和 material buffer 适合做跨 view 稳定结构。visibility buffer 可以把几何可见性、primitive id、material id 或 barycentric 信息写入 per-view layer,后续材质解析阶段共享 material buffer。这样能让材质系统保持单份数据,又能保留每个 view 的可见性差异。对延迟渲染而言,G-buffer 常常按 view 分 layer;材质、纹理和光照参数保持共享;lighting pass 根据 view layer 读取对应 depth、normal 和 motion vector。
资源布局可以按下面的检查表设计。分点顺序按照从输入共享到输出分叉的资源路径排列。
- 场景输入:网格、索引、材质贴图、骨骼姿态和实例列表优先保持单份资源。
- View constants:每个 view 存储 view matrix、projection matrix、camera position、viewport、depth range 和 jitter。
- Render targets:color、depth、normal、motion vector 使用 texture array 或多 attachment layer 存储 per-view 结果。
- Lighting resources:shadow atlas、light list、probe 和环境贴图按是否依赖相机视角决定共享范围。
- Screen-space pass:SSR、SSAO、TAA、motion blur、UI composition 按 view 执行,并保证读取当前 view 的 depth 与 motion vector。
- Submission state:projection layer 或平台提交结构必须引用正确 swapchain image、array layer、viewport rectangle 和 fov。
带宽是资源共享策略的主要约束。双眼 color 和 depth 分辨率较高时,写入带宽、MSAA resolve、post-processing 读取和 compositor 读取都会增加。优化时先统计每个 pass 的 render target 字节数和读写次数,再判断是否值得共享或降分辨率。比如 shadow atlas 共享通常收益高,因为它减少一次完整阴影渲染;screen-space reflection 强行共享容易造成视觉错误,收益也可能被修复成本抵消。
本节的结论是:共享的对象应由“是否依赖当前 view 的屏幕投影”决定。材质和光源这类世界数据稳定共享;屏幕空间结果、最终 depth、motion vector 和透明合成按 view 分离;shadow、reflection、visibility 依据生成视角和可接受误差确定共享粒度。
144.5 沉浸式多视图渲染效果
不同沉浸式显示系统对多视图的要求不同。双眼头显关注两个与头部姿态紧密绑定的 view;AR passthrough 还要把真实摄像头图像、深度、遮挡和虚拟物体对齐;多屏 CAVE 需要为多个物理屏幕生成匹配的投影;光场显示需要更多角度样本,让观察者在小范围移动时看到连续变化的视图。比较这些系统时,统一维度是 view 来源、视图数量、目标布局、校准输入和 compositor 责任。
双眼 VR 是最常见的立体模型。它的 view 数量通常为两个,目标是让头部运动、双眼视差和显示刷新形成稳定闭环。渲染器重点检查 prediction time、eye pose、projection、per-eye depth 和 projection layer 提交。视觉风险集中在视差错误、左右眼交换、late pose 更新失败、帧 pacing 抖动和透明层深度不一致。
AR passthrough 多视图把虚拟图像叠加到真实摄像头或传感器重建结果上。这里的 view 不再只是虚拟眼睛,还涉及相机图像、环境深度、手部遮挡、空间锚点和 compositor 的合成规则。虚拟物体看起来贴在桌面上,需要 tracking space、camera calibration、eye pose、depth occlusion 和 passthrough layer 共同一致。排查时先确认虚拟物体在空间锚点中的位置,再确认 depth occlusion 是否使用同一坐标空间,最后确认提交层的 alpha 与深度规则。
多屏 CAVE 的 view 来自物理屏幕位置。每面墙、地面或投影面都有自己的屏幕平面、观察者位置和 off-axis projection。它和头显双眼的差别在于:一个人可能面对多个固定屏幕,view 的主轴由屏幕几何决定;立体 CAVE 还会为每面屏幕生成左右眼图像。工程上要先校准屏幕角点、投影仪重叠区域和追踪空间,再为每块屏幕生成对应 view。错误表现通常是屏幕接缝错位、地面透视不连续或跨屏物体深度跳变。
光场显示需要多个角度样本。它尝试在一个显示区域中提供多条光线方向,让观察者移动时看到视图连续变化。对实时渲染来说,这会把 view count 从双眼扩展到一组角度采样。直接渲染所有 view 的成本很高,因此工程上常结合分辨率分级、视图重用、深度辅助重投影、神经渲染或离线预计算。质量判断要同时看角度连续性、近处物体遮挡、边缘 ghosting 和细节分辨率。
把这些系统放在同一张工程地图里,可以得到一条稳定判断链:先确定 view 的物理来源,再确定每个 view 的矩阵和目标布局,然后决定哪些资源共享、哪些 pass 分 view 运行,最后把结果提交给负责最终显示的 compositor 或显示控制系统。系统越接近真实世界叠加,校准和空间一致性越重要;系统 view 数量越多,资源共享、culling 分组和分辨率策略越影响性能。
本章的最终判断是:Stereo and Multi-View Rendering 的关键不在“渲染几张图”这个表面动作,而在把 view array、视差模型、GPU 管线、资源共享和显示系统校准连成一个可验证的 frame。一个多视图问题只要能被放回这条 frame 路径,就能把视觉错误拆成可检查的 API 状态、shader 输入、资源 layer、矩阵生成或提交描述。
最小自检任务
给定一个 XR frame:左眼图像正确,右眼图像里所有几何位置整体偏向左侧,透明 UI 在右眼中出现轻微双影。渲染器使用 texture array 存储左右眼 color 与 depth,顶点 shader 通过 gl_ViewIndex 读取 viewProjection[2],projection layer 提交时分别引用 layer 0 和 layer 1。请按多视图 frame 路径说明排查顺序,并判断最可能的错误区域。
答案要点
先检查 view order 与 layer 映射。题目已经说明提交引用 layer 0 和 layer 1,但仍要确认 layer 1 的 framebuffer、render pass view mask 或 layered rendering 规则确实写入右眼。第二步检查 viewProjection[1] 的内容,右眼所有几何整体偏向左侧,说明右眼矩阵可能沿用了左眼 projection、使用了错误 eye pose,或 IPD 符号反向。第三步检查透明 UI 的空间类型和 depth 输入,轻微双影说明 UI 可能用头部空间固定位置叠加,同时缺少与右眼一致的深度或使用了单眼 depth。核心结论是:几何整体偏移优先定位到 per-view constants 与 eye pose/projection;透明层双影再检查 composition layer、UI 深度和 per-eye post-processing。必要边界是:如果右眼完全显示左眼画面,资源 layer 绑定更可疑;如果两眼一起随头动漂移,tracking space 和 prediction time 更可疑。
本章知识点总结
- View array:多视图 frame 由多个 view 组成,每个 view 拥有自己的姿态、投影、目标 layer 和提交描述。
- 共享输入:网格、材质、实例、光源和大部分世界数据可以跨 view 共享。
- 每眼状态:eye pose、projection、camera position、depth、motion vector 和 screen-space 结果通常按 view 分离。
- 视差来源:双眼深度感主要来自 IPD、每眼 pose、projection 和显示校准共同形成的图像差异。
- 尺度检查:XR 场景应把物理距离和世界单位对齐到米,尺度错误会直接改变视差和深度感。
- 矩阵排查:单眼整体漂移时,优先检查 view-projection 数组、下标规则、IPD 符号和 fov 来源。
- Multiview:view instancing 或 multiview 能减少重复提交和状态绑定,但每个 view 仍有自己的裁剪、光栅化和像素输出。
- Culling 策略:双眼头显适合共享 union frustum 起步,宽视角多屏或光场系统需要按 view 组细分可见集。
- Depth 边界:最终 depth 像素依赖当前 view,资源管理和 pass 逻辑可以共享,depth layer 结果需要分 view 保存。
- Shadow 共享:shadow map 多数情况下由光源视角决定,适合跨 view 共享。
- Reflection 边界:环境反射可共享,screen-space reflection 通常依赖当前 view 的 depth 与 color。
- 沉浸系统:VR、AR passthrough、CAVE 和光场显示的差异来自 view 来源、校准输入、目标布局和 compositor 责任。
- 工程路径:多视图问题应沿 runtime view state、矩阵生成、culling、draw、layer 输出和 frame submission 逐级定位。