在 XR 中物体通常需要以立体(in sterro)的方式被渲染,即一个内容需要被绘制到左眼和右眼的纹理中,这种渲染方式被称为 立体渲染(Stereo-Rendering),本文将介绍几种立体渲染的方式,包括:

  • Multi-Pass:左眼和右眼画面由两张纹理构成,并分别绘制左眼和右眼画面。这是最原始,兼容性最好的渲染方式,但拥有较差的性能。
  • Single-Pass:左眼和右眼由一个纹理构成,通常是目前常规应用首先的渲染方式,拥有较好的性能。该模式有较多的变种,如 Single-Pass Double WideSingle-Pass InstancedSingle-Pass Multi-View 等。
  • Quad-View:一种较新的渲染方式,将左右眼画面进一步拆分为 Inner-LeftInner-RightOuter-LeftOuter-Right 四个视图,通过 InnerOuter 不同的 PPD(Pixels Per Degree)来提升画面的清晰度。该模式通常在支持眼动追踪的设备上使用。

#Multi-Pass

在 Multi-Pass 渲染模式下,需要为每个眼睛创建一个单独的渲染纹理(Texture2D),并在每个眼睛的渲染循环中分别绘制左眼和右眼的画面。Multi-Pass 渲染模式的优点是兼容性好,支持所有 XR 设备,但性能较差,因为针对每一个物体都需要进行两次渲染,此外,每一次渲染时都需要切换渲染纹理(这就是被称为多 Pass 的原因)。

其示意图如下所示

#Single-Pass

顾名思义,Single-Pass 就是渲染目标只有一张纹理,在这个前提下,Single-Pass 有多种变种模式,每一个变种都有其自己的渲染方式:

一个常见的误解是认为 Single-Pass 就是同一目标一次渲染完成左眼和右眼画面,Multi-Pass 是两次渲染完成左眼和右眼画面。
实际上,Single-Pass 和 Multi-Pass 的区别在于渲染目标的数量,Single-Pass 只有一张纹理,而 Multi-Pass 有多张纹理。在 single-Pass Double Wide 的情况下,Single Pass 同样需要两次渲染来完成左眼和右眼画面的渲染。

#Single-Pass Double Wide

Single-Pass Double Wide 方案用一张宽度是单个眼睛画面两倍的纹理来同时表示左眼和右眼画面,其中左半部分是左眼,右半部分是右眼。其示意图如下所示:

可以看到在 Single-Pass Double Wide 方案下:

  • 每个对象仍然需要被 两次 渲染至左眼和右眼部分,只不过现在是通过一个纹理来表示左眼和右眼画面。
  • 渲染采用了 ping-pong 的方式,即渲染的目标会在左右眼部分以 Left-Right-Right-Left-Left-.... 的方式切换(见上示意)。这样的好处只需要遍历一次所有需要的对象,即能完成左右眼的渲染,也因此减少了渲染时上下文的切换和需要执行的 Command 数量。

Single-Pass Double Wide 方案基本已经被淘汰,因为它仍然需要对每个对象做两次渲染,且其 ping-pong 的方式会极大的增加管线复杂度。

目前的设备通常都支持 Single-Pass Instanced 或 Single-Pass MultiView,这两种方案也有更高的性能。

#Single-Pass Instanced / Single-Pass MultiView

Single-Pass Instanced 和 Single-Pass MultiView 方案都是用一张纹理数组来表示表示左眼和右眼画面,其中 Index 0 是左眼,Index 1 是右眼。该两者的优点是都是节省了纹理切换的开销,且每个物体可以通过 一次 渲染同时被绘制到左眼和右眼画面上,其缺点都在于兼容性,这两种默认都要求设备需要支持特定的 GPU 特性,以及渲染时的 Shader 需要做一定的适配。

这两种方案所需要的特性和 Shader 的修改都是为了两个目的:

  1. 可以通过一次渲染同时绘制到左眼和右眼画面上,但绘制时需要区分左右眼的 View Matrix
    • Instanced:使用 GPU 的 Instancing 特性来实现,使用 Instance ID 区分左右眼的 View Matrix
    • MultiView:依靠 MultiView 拓展[1],使用其中的 gl_ViewID_OVR 区分左右眼的 View Matrix
  2. 在渲染时可以指定渲染目标纹理的 Index
    • Instanced:依赖各平台的 GPU 特性来实现,如 DirectX 11 的 VPAndRTArrayIndexFromAnyShaderFeedingRasterizer,OpenGL 的 GL_NV_viewport_array2, GL_AMD_vertex_shader_layer, GL_ARB_shader_viewport_layer_array
    • MultiView:依靠 MultiView 拓展[1:1],使用其中的 FramebufferTextureMultiviewOV 来指定渲染目标纹理的 Index

指定渲染目标纹理的 Index 的 Instanced 拓展,如 GL_NV_viewport_array2 等在移动端 OpenGLES 下并不支持,所以在移动端为了实现渲染到目标纹理的不同 Index 的目的,必须依赖 MultiView 拓展。

Instanced 方案和 MultiView 方案几乎就是同样的实现目标的两种方式,因为特性支持的不同,Instanced 方案在 PC 端的支持更好,而 MultiView 方案在移动端的支持更好。
为方便描述,后续不再区分 Instanced / Multiview,直接称为 Multiview。

#Quad-View

Quad-View 是一种较新的渲染方式,它是 Foveation Rendering 的一种实现方式,它将左右眼画面进一步拆分为 Inner-LeftInner-RightOuter-LeftOuter-Right 四个视图。Outer 画面使用原始的 Fov 进行渲染,Inner 画面则是较小的 FOV,这样即使 InnerOuter 的分辨率相同,Inner 画面也会拥有较高的 PPD(Pixels Per Degree),也因此 Inner 画面会有更高的清晰度。

下图展现了同一个场景内中,单一眼的 Outer 和 Inner 画面,这里 Outer 的横向 FOV 约为 90°,Inner 的横向 FOV 约为 40°:
Outer vs Inner

Quad-Views 通常在支持眼动追踪的设备上使用,Inner 画面的渲染区域由眼动注视的区域来决定。

针对 Quad-View 的四个视图的渲染方式又有两个变种:

  • Single-Pass:通过 一个 Array Size 为 4 的纹理数组来表示四个视图,使用 Multview 的方式一次性更新四个视图的画面。
  • Multi-Pass:通过 两个 Array Size 为 2 的纹理数组来表示 Inner 的左右眼画面和 Outer 的左右眼画面,使用两次绘制分别更新 Inner 和 Outer 的画面,每次绘制都使用 Multview 的方式来同时更新左右眼画面。

理论上,QuadViews 还可以有更多的组合方式,如:

  • 通过 4 个 Texture2D 来表示四个视图,使用 Multi-Pass 的方式通过四个 DrawCall 来更新四个视图的画面。
  • 使用 2 个 Texture2D 表示 Outer 的左右眼,再使用一个 Array Size 为 2 的 Texture2DArray 表示 Inner 的左右眼。先通过两个 DrawCall 更新左右眼 Outer,再通过一个 DrawCall 更新左右眼 Inner。
    但这些组合方式并没有带来额外的收益,因此这里不进行讨论

在 Unity XR 中,最多支持 ArraySize 为 2 的 Texture2DArray,因此在 Unity 中 Quad-View 只能通过 Multi-Pass 的方式来实现。

Single-Pass 和 Multi-Pass 有各自的好处,也因此这两种方案都有各自适合的场景:

  • Single-Pass 方案的性能更好,但它限制所有的视图必须是一样的分辨率。
  • Multi-Pass 则是牺牲了一部分性能,却让 Inner 和 Outer 可以有各自的分辨率

无论 QuadViews 使用 Single-Pass 还是 Multi-Pass,其都依赖 MultiView 渲染相应支持。

#Quad-Views with Single-Pass

在 Quad-Views with Single-Pass 模式下,Inner/Outer 针对每个物体通过一次 DrawCall 同时更新到四个 View 中,示意图如下所示:

#Quad-Views with Multi-Pass

在 Quad-Views with Multi-Pass 下,每一个 DrawCall 会分别将内容绘制到 Outer(或 Inner) 的左右眼画面上,示意图如下所示:

#Reference

Quad-Views Foveated

Single Pass Stereo rendering (Double-Wide rendering)


  1. OVR_multiview ↩︎ ↩︎