剖析虚幻渲染体系(15)- XR专题
阅读原文时间:2022年06月09日阅读:10

目录

15.1 本篇概述

虚拟现实(VR)因伊万·萨瑟兰(Ivan Sutherland)在20世纪60年代的工作而广受赞誉。在过去的50年里,虚拟现实技术的普及程度上下波动。特别是,近年来,虚拟现实在虚拟现实及其对应产品增强现实(AR)方面的投资吸引了许多科技巨头的注意。例如,2014年,Facebook以20亿美元收购了虚拟现实技术公司Oculus,并开始推动虚拟现实进入新时代。如今,苹果、谷歌、索尼和三星等所有主要厂商都在这一领域投入了大量资金,努力让虚拟现实变得可访问且价格合理。

本篇主要阐述XR的以下内容:

  • XR概述
  • XR技术
  • XR的引擎集成
  • XR生态

XR涉及的概念及关系如下两图:

AR、VR的技术对比如下:

15.1.2.1 VR

什么是虚拟现实?让电子世界看起来真实且互动,非静态3D图像,不是电影,可以在3D世界中移动,可以在3D世界中操纵对象。虚拟现实体验包含浸入式空间和沉浸式体验两种模式。其中浸入式空间具有360度全景图像/视频、高视觉质量、有限的互动性、改变视点方向、用户可以转头看到不同的视图、固定位置等特点。沉浸式体验具有三维图形、低视觉质量、高交互性、太空运动、与虚拟对象交互等特点。VR设备包含基于PC和基于移动端两种方式。

虚拟现实硬件包含头戴式显示器、运动跟踪、头部追踪、控制器等硬件部件。

早期的VR设备包含Desktop VR、立体拷贝(Stereoscopy)、头部耦合透视(HCP)、头盔显示器(Head-mounted display,HMD等几种模式,它们的技术对比如下:

虚拟现实硬件类型有PC HMD、移动HMD(一体机)、手机嵌入设备三种类型。

PC HMD指的是作为外部显示器的桌面外围设备,提供最深度、最沉浸式的虚拟现实,跟踪位置和方向,使用一条或多条电缆连接到计算机,用于跟踪位置。

移动HMD一般是定制Android build/Oculus mobile SDK,仅限方位跟踪,支持即将到来的S6–三星Gear VR,支持LG G5(LG VR),110对角视野(Gear VR),1000hz刷新率(Gear VR)。

手机嵌套设备是移动虚拟现实开放规范,仅限方位跟踪,标准Android、iOS支持,使用简单的立体渲染和加速度计跟踪,只需添加智能手机,90度视场(硬纸板),200hz刷新率。

可扩展3D(X3D)图形是在Web上发布、查看、打印和存档交互式3D模型的免版税开放标准,X3D和HAnim标准由Web3D联盟开发和维护。使用X3D的HMD虚拟现实服务如下图:

衡量VR设备的基本参数包含立体绘制、6DOF(6自由度,包含位置和朝向)、视场(FOV)等,下表是2017年前后的VR设备的基本参数:

VR设备的经典代表Quest 2的参数如下:

  • 面板类型:快速开关LCD。
  • 显示分辨率:每只眼睛1832x1920。
  • 支持刷新率:60Hz、72Hz、80Hz、90Hz。
  • 默认SDK颜色空间:Rec.2020 gamut,2.2 gamma,D65 white point。
  • USB接口:1x USB 3.0。
  • 跟踪:由内向外,6自由度。
  • 音频:集成,内置。
  • SoC:高通Snapdragon XR2平台。
  • 内存:总计6GB。
  • 镜头距离:可调IAD,有58、63和68mm三种设置。

15.1.2.2 AR

增强现实(AR)通过将虚拟对象叠加到真实世界中,无缝地融合了真实世界和虚拟世界,与用计算机模拟的虚拟世界代替真实世界的VR不同,AR改变了人们对真实世界的持续感知,Pokémon Go和Snapchat过滤器是AR的两个示例。AR通过将我们所看到的与计算机生成的信息叠加,增强了我们对现实世界的看法。如今,这项技术在智能手机AR应用程序中非常流行,这些应用程序要求用户将手机放在面前。通过从相机中拍摄图像并实时处理,该应用程序能够显示上下文信息或提供似乎植根于现实世界的游戏和社交体验。

虽然智能手机AR在过去十年中有了显著改善,但其应用仍然有限。人们越来越关注通过可穿戴智能眼镜提供更全面的AR体验。这些设备必须将超低功耗处理器与包括深度感知和跟踪在内的多个传感器结合在一起,所有这些都必须在一个足够轻和舒适的外形范围内,以便长时间佩戴。

AR智能眼镜需要在用户移动时始终开启、直观且安全的导航。这需要在深度、遮挡(当三维空间中的一个对象挡住另一个对象的视线时)、语义、位置、方向、位置、姿势、手势和眼睛跟踪等功能方面取得关键进展。

2021,许多新型智能眼镜上市,包括Snap的眼镜智能眼镜、联想ThinkReality A3和Vuzix的下一代智能眼镜。AR智能眼镜旨在改善我们未来的生活,如以下视频所述。它们还可能扮演通向虚拟元素和现实相交的元宇宙的门户的角色。

15.1.2.3 MR

混合现实(MR)是VR和AR技术的混合体,MR有时被称为混合现实,它将真实世界与虚拟世界融合在一起,在虚拟世界中,真实和数字对象可以共存并实时交互。与AR类似,MR将虚拟对象叠加在真实世界的顶部。与VR类似,这些叠加的虚拟对象是交互式的,使用户能够操纵虚拟对象。微软HoloLens就是MR的一个很好的例子。MR位于AR和VR之间,因为它融合了真实世界和虚拟世界。这种类型的XR技术有三个关键场景。第一种是通过智能手机或AR可穿戴设备,将虚拟对象和角色叠加到真实环境中,或者可能反过来。

2016年风靡全球的Pokémon Go手机游戏通过智能手机摄像头在现实世界中覆盖虚拟的神奇宝贝),它经常被吹捧为革命性的AR游戏,但实际上它是MR的一个很好的例子——将真实世界的环境与计算机生成的对象混合在一起。混合现实技术也开始被用于将VR真实世界的玩家叠加到视频游戏中,从而将真实世界的个性带入Twitch或YouTube等游戏流媒体平台。

15.1.2.4 XR

扩展现实(eXtended Reality,XR)是一个“包罗万象”的术语,指增强或取代我们世界观的技术,通常通过将计算机文本和图形叠加或浸入到真实世界和虚拟环境中,甚至是两者的组合。XR包括增强现实(AR)、虚拟现实(VR)和混合现实(MR),虽然这三种“现实”都有共同的重叠特性和需求,但每种都有不同的目的和底层技术。

XR被设定为在元宇宙(metaverse)中发挥基本作用。“互联网的下一次进化”将把真实、数字和虚拟世界融合到新的现实中,通过一个Arm驱动的“网关”设备(如VR设备或一副AR智能眼镜)进行访问。XR技术有一些基本的相似之处:所有XR可穿戴设备的核心部分是能够使用视觉输入方法,如对象、手势和注视跟踪,来导航世界和显示上下文相关信息,深度感知和映射也可以通过深度和位置功能实现。然而,XR设备根据AR、MR和VR体验的类型以及它们设计用于启用的用例的复杂性而有所不同。

虚拟现实的历史通常可以追溯到20世纪60年代,这个概念甚至可以追溯到1938年以后。也就是说,当时的虚拟现实与我们现在的虚拟现实非常不同。第一款广为人知的虚拟现实头戴式显示器(HMD)是达摩克利之剑,由计算机科学家伊万·萨瑟兰(Ivan Sutherland)和他的学生鲍勃·斯普鲁尔(Bob Sproull)、奎汀·福斯特(Quitin Foster)和丹尼·科恩(Danny Cohen)发明。虚拟现实系统要求用户戴上安全带,因为整个设备对用户来说太重了,这种不可行性使得达摩克利之剑的使用仅限于实验室。

从那时起,VR HMD就开始发展。2019年,Facebook发布了其独立无线虚拟现实HMD Oculus Quest,用户不再需要PC或手机来操作和使用基于摄像头的位置跟踪(Oculus Insight)。这款内置电脑单元的新型设备为虚拟现实带来了一个移动和自由的新时代,Oculus Quest设备在一周内就在多家零售店售罄,在前两周,VR内容销售额达到500万美元。

2020年,近50%的AR/VR支出用于商业用例,其中26亿美元用于培训,9.14亿美元用于工业维护。消费者支出占AR/VR总支出的三分之一,VR游戏和VR功能观看分别为33亿美元和14亿美元。关于全球AR和VR支出的预测,2019年至2023年,前三位最快的支出增长率分别为大专实验室和现场,复合年增长率(CAGR)为190.1%,K-12实验室和现场,复合年增长率为168.7%,现场组装和安全,复合年增长率为129.5%。培训用例将是2023年最大的预测支出。

鉴于虚拟现实技术的发展潜力,苹果、谷歌、微软、Facebook、三星、IBM和其他主要公司在2020年对虚拟现实技术进行了大量投资。虚拟现实肯定有很有前途的支持者和信徒。将虚拟现实用于商业,主要是通过增强购物者的体验,也是一种趋势。通常,虚拟工具用于增强现实世界的环境,帮助购物者在实体商店中定位商品。虚拟现实技术还允许购物者定制他们感兴趣的产品。更有趣的是,购物者现在可以像在现实生活中一样,远程、虚拟地浏览商店的数字孪生兄弟,购买产品。

近年来,虚拟现实技术的全球增长势头强劲,部分原因是其迅速被消费者市场接受,其主要需求来自游戏、娱乐和体验活动行业。虚拟现实改变了我们消费内容的方式,给用户带来了更具互动性和沉浸式的体验。预计到2022年,全球虚拟现实产业预计将达到2092亿美元。随着入门成本的下降和全面部署带来的好处变得更加明显,AR/VR的商业应用将继续扩大。重点正在从谈论技术好处转向展示真实和可衡量的业务成果,包括生产力和效率的提高、知识转移、员工的安全,以及更具吸引力的客户体验。

XR的简要历史。

VR好的用户体验体现在吸引人、参与感和值得纪念的时刻等三方面。其中参与感包含了在场、联系、记忆三方面,这也是使用VR的良好理由。而反面的例子是阅读、键入和精准操作。

好的VR应用应当选择正确的技术,满足在达成和质量、续航、内容发布、备份、后勤等方面的需求。

此外,需要遵循VR最佳实践,具体如下:

保持玩法短暂且清晰:

还是有很多用户对VR不甚了解,是新手,需要对其进行测试,使得模拟负面影响尽量真实,进行跨年龄和视力测试,没有明确的用户体验范例。对于移动端VR,由于开发新硬件,但用户有旧硬件,延迟令人眩晕,设备、型号、版本之间存在差异。对于房间规模VR(room-scale VR),进行压力测试、跨GPU测试及存储、内存测试等。

假设每个人都是VR新手,需要耐心、温柔,解释会发生什么,告诉他们工作原理,和他们呆一分钟,监控其进度,获取反馈,短信提醒,建议转换。VR并不是即时直观的,需要引导他们。品牌需要注意10个VR提示:

为了最小化VR的负面影响(眩晕),需要快速帧速率(90以上最佳),当头部移动(20ms或更短)时,将延迟降至最低,正确获取所有视觉线索,最小化加速度,基于我们的视野和前庭系统如何相互作用的创造性解决方案。其中避免加速度尽量使用恒定速度,即时更改,如果是曲线,则提前显示,显示轨迹,减速,尽量远程传输,但显示地标,让玩家控制。注视点视野中心2度视野,周边视觉是运动的关键,快速移动时模糊或消除周边视觉。

眼睛追踪使注视点渲染成为可能,来到VR以及最终的移动领域,关键是学习我们的视觉系统/大脑如何相互作用,最起码需要什么?VR需要很多因素来保证正确和良好的体验,如帧速率、头部跟踪、视野范围、收敛性(Vergence)、3D渲染、透视、视差、距离雾、纹理、大小、遮挡…以及更多。下图是VR设备在显示、光学等方面的趋势预测:

渲染技术的预测如下:

制约VR体验的因素包含以下几方面:

谨防使用浮动图标、界面破坏存在感,将界面放入3D世界数字界面,研究我们的视觉系统,少即是多保持高帧率,不需要真实感。2017到2019年的VR设备市场份额的变化趋势如下图:

Oculus售出了150万台Rift,拥有12款100万美元以上的OCULUS游戏,OCULUS QUEST发布且售价在399美元。现象级的游戏代表是节奏光剑(Beat Saber):

对于Oculus而言,未来的目标是达到十亿级的VR用户:

VR/AR行业覆盖了硬件、系统、平台、开发工具、应用以及消费内容等诸多方面。作为一个还未成熟的产业,VR/AR行业的产业链还比较单薄,参与厂商(尤其是内容提供方)比较少,投入力度不是太大。核心内容生产工具面临较大的研发制作瓶颈,如360°全景拍摄相机,市面上的产品屈指可数。

当前XR涉及了硬件、OS、引擎、设备制造等等产业,涉及的公司和品牌数不胜数:

随着近年来,投资圈在XR圈的活跃,相信生态圈会越来越完善,正如2010年前后的智能移动设备。

当前,由于Covid -19大流行,世界其范围正面临危机,例如,中国的在家工作场景要求新的工作或协作方式。VR为传统工作模式提供了另一种解决方案,可以实现虚拟会议、化身、面对面等办公和沟通需求。

自互联网诞生以来,信息通信和媒体技术一直呈指数级增长,虚拟现实技术创新发挥着重要作用。过去,虚拟现实主要是在消费游戏领域。虽然这一趋势将继续下去,但我们看到虚拟现实商业应用的快速增长。这主要是由于消费者习惯的改变以及虚拟现实硬件成本的大幅降低。与几十年前相比,虚拟现实的进入门槛相对较低,这使得虚拟现实成为了一个可行的解决方案,并增强了多个用例段。

为了提高生产力、提高效率和降低运营成本,新加坡的许多企业都在将最新的虚拟现实技术应用于从培训到工作的各种应用中。另一方面,普通消费者,尤其是年轻人,越来越愿意使用虚拟现实以增强他们不同的消费体验,如购物和娱乐。

政府机构也在将虚拟现实技术纳入其一些关键业务中。例如,警察和民防部门在培训中采用了虚拟现实技术。VR是某些政府建筑项目的强制要求。我们将进一步阐述这些应用在以下领域的沟通、协作与协调、培训以及可视化。

XR应用领域主要体现在(但不限于):

  • 沟通、协作与协调。有了互联网,可以实时与不同地理位置的人合作。然而,虚拟现实带来了一种新的沟通和协作方式。它通过让人们更加接近彼此,为虚拟现实视频会议创造了沉浸式的互动体验。沉浸在虚拟现实环境中,人们可以在没有身体干扰的情况下更加专注于会议。研究表明,与传统视频会议相比,虚拟现实沉浸式会议的注意力预计会增加25%。在旅行成本和物理会议空间方面,还有其他可观的节约。鉴于新冠肺炎的流行和社会疏远措施,许多社会和社区功能和活动已被完全取消。其中一些活动对参与者来说非常重要,诸如集会仪式之类的活动可以在虚拟现实空间中进行,毕业生和活动参与者可以使用手机和虚拟现实设备参加来自世界各地的虚拟毕业典礼。
  • 培训。虚拟现实经常用于培训,因为它允许学员在沉浸式安全环境中体验真实情况。传统的动手培训通常需要物理设备、空间和操作停机时间。在某些情况下,受训人员可能会接触到他们没有准备好的工作场所危险。有了虚拟现实,培训可以随时随地进行,减少了资源成本和等待时间。培训师还可以定制虚拟现实环境,对员工进行不同场景的培训,让学员掌握大量知识,以解决各种问题。例如,犯罪现场的第一反应人员可以接受培训,以处理大量模拟场景,并反复磨练他们的决策技能。与传统场景模型相比,这种实现还允许主队更有效地使用物理存储和空间,允许更多的人更频繁地接受培训,而传统场景模型需要物理道具、模型和昂贵的空间。
  • 可视化。在AEC业务中,虚拟现实帮助用户在网站建设之前将其设计可视化,从而节省成本并产生更好的效果。从2017年到2019年,虚拟现实在虚拟设计与施工(VDC)领域的招标要求有所增加。如果没有完全排除在此类项目竞争之外,没有VDC能力的公司将处于巨大劣势。在建筑设计师开始使用虚拟现实技术之前,他们可以先验证虚拟现实技术的优势,这一过程可以节省大量成本,并在建筑建成前缓解安全问题。
  • 游戏和娱乐。VR游戏、影视等方面的应用是推动VR发展的主要动力之一,以节奏光剑等游戏的流行也推广了VR设备的普及。随着VR技术最初在游戏行业的兴起,VR将对游戏产生巨大影响也就不足为奇了。XR为玩家提供富有吸引力的虚拟对象,丰富游戏环境,允许远程玩家在同一游戏环境中实时游戏和交互,允许玩家通过身体运动改变游戏中的位置,允许游戏从二维空间移动到三维空间。
  • 流媒体。XR带来身临其境的体验,并通过6个自由度(6DoF)的能力增强媒体流媒体体验。这允许用户在虚拟现实环境或体育赛事或音乐会中移动并与之交互。

如今的XR仍处于起步阶段,类似于10年前的智能手机,XR的发展将需要数年时间……但机会将是巨大的。

总之,随着国家向数字经济迈进,虚拟现实技术已经在多个行业被广泛采用和使用。商业和消费市场对虚拟现实技术的需求将继续增长。在未来的几年里,虚拟现实将被广泛应用于每一个国人的日常生活和公司的运营中,因为它变得更容易获得和负担得起。出于这个原因,预计商业市场会发生轻微变化,因为一些人可能会开始从虚拟现实转向AR,或者未来的两年肯定是虚拟现实的关键时期,因为科技巨头正在努力为现有的虚拟现实应用带来新的增强,并进一步提高沉浸式技术的技术上限。

15.2 XR技术

桌面虚拟现实(Virtual Reality,VR)历来是消费级3D计算机图形的主要显示技术。近来,立体视觉和头戴式显示器等更复杂的技术已变得更加普及。然而,大多数3D软件仍然仅设计用于支持桌面VR,并且必须进行修改以在技术上支持这些显示器并遵循其使用的最佳实践。需要评估现代3D游戏/图形引擎,并确定了它们在多大程度上适应不同类型的负担得起的VR显示器的输出,表明立体视觉得到了广泛的支持,无论是原生还是通过现有的适应。其它VR技术,如头戴式显示器、头部耦合透视(以及随之而来的鱼缸VR)很少得到原生支持。

2013年虚拟现实显示技术有桌面VR(串流)、立体视觉、头部耦合透视、头戴式显示器等几种,它们在模拟模型和用户感知方面的差异如下图:

立体视觉(Stereoscopy)是适用于双目视觉的桌面VR范式的扩展。立体镜通过两次渲染场景来实现这一点,每只眼睛一次,然后以这样的方式对图像进行编码和过滤,使每张图像只能被用户的一只眼睛看到。这种过滤最容易通过特殊的眼镜实现,眼镜的镜片设计为选择性地通过匹配显示器产生的两种编码之一。当前的编码方法是通过色谱、偏振、时间或空间。这些编码方法经常被分类为被动、主动或自动立体。被动和主动编码之间的区别取决于眼镜是否是电主动的:因此被动编码系统是颜色和极化,而唯一的主动编码是时间。自动立体显示器是不需要眼镜的显示器,因为它们在空间上进行编码,这意味着眼睛之间的物理距离足以过滤图像。

消费者立体显示器与计算机的接口方式与桌面VR显示器相同(通过VGA或DVI等视频接口)。由于这些接口中的大多数都没有特殊的立体观察模式,因此将两个立体图像以显示硬件可识别的格式打包成一个图像。此类帧封装格式包括交错、上下、并排、2D+深度和交错。由于这些标准化接口是软件将渲染图像传递给显示硬件的方式,因此软件应用程序不需要了解或适应编码系统的显示硬件。相反,图形引擎支持立体透视所需要的只是它能够从不同的虚拟相机位置渲染两个具有相同模拟状态的图像,并将它们组合成显示器支持的帧封装格式。

头部耦合透视(Head-coupled perspective,HCP) 的工作原理与桌面VR和立体视觉略有不同, 定义了一个虚拟窗口而不是虚拟相机,其边界是虚拟的窗口映射到用户显示器的边缘。因此,显示器上的图像取决于用户头部的相对位置,因为来自虚拟环境的对象会沿用户眼睛的方向投影到显示器上。这种投影可以使用桌面VR中使用的投影数学的离轴版本来完成。

为了做到这一点,必须实时准确地跟踪用户头部相对于显示器的位置。用于此目的的跟踪系统包括电枢、电磁/超声波跟踪器和图像- 基于跟踪。HCP的一个限制是,由于显示的图像取决于用户的位置,因此任何其他观看同一显示器的用户将感知到失真的图像,因为他们不会从正确的位置观看。

头戴式显示器(Head-mounted display,HMD)是另一种单用户VR技术,将立体视觉的增强功能与类似于HCP的大视场和头部耦合相结合。HMD背后的感知模型是完全覆盖用户眼睛的视觉输入,并将其替换为虚拟环境的包含视图。通过将一个或两个小型显示器安装在非常靠近用户眼前的镜头系统来实现的,以实现更自然的聚焦。由于显示器非常靠近用户的眼睛,显示器的任何部分只有一只眼睛可见,使系统具有自动立体感。

头饰中还嵌入了一个方向跟踪器,允许跟踪用户头部的旋转,允许用户通过将虚拟相机的方向绑定到用户头部的方向来使用自然的头部运动来环顾虚拟环境。它与HCP不同,HCP跟踪的是位置,而不是方向。支持HMD的软件要求与立体观察相同,但附加要求是图形引擎必须考虑HMD的方向,以及要校正的镜头系统引起的任何失真。

通过确定可以使用哪些扩展机制来实现所需的VR显示技术来衡量支持级别,已经结合了差异可以忽略不计的扩展机制(例如脚本和插件),并引入了两个额外的级别,不需要扩展(本机支持)和没有引擎内支持(重新设计)。扩展机制按引擎代码相对于实现VR支持的非引擎代码的比例排序,产生的支持级别及其排序如下:

5、原生支持。在原生支持VR技术的引擎中,引擎的开发人员特意编写了渲染管线,使用户只需最少的努力即可启用VR渲染。所需要做的就是检查开发人员工具中的选项或在引擎的脚本环境中设置变量。除了轻松启用该技术外,这些引擎还旨在避免常见的优化和快捷方式,这些优化和快捷方式在桌面VR显示器中并不明显,但随着更复杂的技术变得明显,一个常见的例子是渲染具有正确遮挡但深度不正确的对象,会导致立体镜下的深度提示冲突。

4、通过引擎内图形定制(包括节点图)。一些引擎的设计方式使得可以使用具有图形界面的自定义工具来更改渲染过程,一种方法是通过节点图,其中渲染管线的不同组件可以在多种配置中重新排列、修改和重新连接。根据支持的节点类型,有时可以配置节点以产生某些 VR 技术的效果。下图显示了虚幻引擎的材质编辑界面,该界面配置为将红青色立体立体渲染作为后处理效果。

3、通过引擎内编码(脚本或插件)。每个引擎都可以使用自定义代码进行扩展,使用定义明确但受限制的扩展点。两种常见的形式是在受限环境中运行的脚本,以及引擎加载并运行外部编译的代码插件,两种形式都可以访问引擎功能的子集,但是,插件也可以访问外部API,而脚本不能。由于通常实现特定于应用程序功能的机制,因此可用于自定义代码的引擎功能可能更多地针对人工智能、游戏逻辑和事件排序,而不是控制确切的渲染过程。

2、通过引擎源代码修改。除了免费的开源引擎,一些商业引擎通过适当的许可协议向用户提供其完整的源代码。通过访问完整的源代码,可以实现任何VR技术,尽管所需的修改量可能很大。

1、通过工程改造。对于不提供上述任何定制入口点的引擎,仍然可以通过重新设计进行一些更改。工程改造是逆向工程的一种形式,除了学习程序的一些工作原理之外,还修改了它的一些功能。对渲染管线进行完全逆向工程所需的工作量可能很大,因此更可取的是微创形式的再工程。其中一种方法是函数挂钩,即内部或库函数的调用被拦截并替换为自定义行为。由于很大一部分实时图形引擎使用OpenGL或Direct3D库进行硬件图形加速,因此这些库为通过函数挂钩实现纯视觉VR技术提供了可靠的入口点。事实证明,这种方法可以有效地将立体视觉添加到3D游戏。本文还展示了以这种方式实现头耦合透视也是可能的,通过挂钩加载投影矩阵(glFrustum和glLoadMatrix)的OpenGL函数,并用头部耦合矩阵替换原始程序提供的固定透视矩阵。

影响用户体验的因素有很多,虽然质量因素本质上与显示硬件相关,但适当的软件设计可以缓解这些问题,而粗心的设计可能会引入新问题。可以通过软件减轻的硬件质量因素的示例是串扰(立体)、A/C故障(立体)和跟踪延迟(HCP和HMD)。由于这些因素对于它们各自的显示技术来说是公认的,因此有众所周知的技术可以最大限度地减少它们引起的问题。解决方案分别是降低场景对比度、降低视差和最小化渲染延迟。

不正确的软件实现也会影响VR效果的质量,可能是由于粗心或桌面VR优化的结果。这方面的一个示例是任意位置的特殊图层(例如天空、阴影和第一人称玩家的身体)不同通道的深度。虽然在桌面VR中产生正确的遮挡,但在立体镜下添加双目视差提示会显示不正确的深度,并在这两个深度提示之间产生冲突。由于桌面VR的主导性质,这不是一个不常见的问题,并且可以作为另一个例子,说明简单的第三方实现可能不如原生VR支持。从这些方面应该注意到,虽然非原生VR实现可能满足必要的技术要求,但也必须考虑其它因素。

下表是2013年的主流引擎对VR的支持情况:

引擎

立体视觉

头部耦合透射

头戴式显示器

UDK

4:图形定制。可以使用Unreal Kismet创建双摄像头装备,并使用材质编辑器打包输出。

1:工程改造。无法从引擎访问自定义相机投影,因此如果无源代码访问权限,则需要工程改造。

3:引擎编码。通过自定义实现立体化,可以通过自定义DLL获得头部方向并通过脚本绑定到相机。

Unity

3:引擎编码。

3:引擎编码。

3:引擎编码。

CryENGINE

5: 原生。

3:引擎编码。

3:引擎编码。

OGRE

3:引擎编码。

3:引擎编码。

3:引擎编码。

虚拟现实中最重要的因素有:短余辉(Low Persistence)、延迟、现实。

VR的软件和硬件架构通常有好几层:C/C++接口、驱动程序DLL、VR服务层(在各应用之间分享和虚拟现实转换)等,下图是Oculus早期的架构图:

SDK的一般工作流程(以Oculus为例):

  • ovrHmd_CreateDistortionMesh。通过UV来转换图像,比像素着色器的渲染效率更高,让Oculus能更灵活地修改失真。
  • ovrHmd_BeginFrame。
  • ovrHmd_GetEyePoses。
  • 基于EyeRenderPose(游戏场景渲染)的立体渲染。
  • ovrHmd_EndFrame。

Oculus SDK易于集成,无需创建着色器和网格,通过设备/系统指针和眼睛纹理,支持OpenGL和D3D9/10/11,必须为下一帧重新申请渲染状态。好处:与今后的Oculus硬件和特性更好地兼容,减少显卡设置错误,支持低延迟驱动显示屏访问,例如前前缓冲区渲染等,支持自动覆盖:延迟的测试、摄像头指南、调试数据、透视、平台覆盖。支持Unreal Engine 3、Unreal Engine 4、Unity等主流游戏引擎使用SDK渲染。支持扩展模式:头戴设备显示为一个OS Display,应用程序必须将一个窗口置于Rift监视器上,图标和Windows在错误的位置,Windows合成器处理Present,通常有至少一帧延迟,如果未完成CPU和GPU同步,则有更多延迟。另外,它支持Direct To Rift的功能,即输出到Rift,显示未成为桌面的一部分。头戴设备未被操作系统看到,避免跳跃窗口和图标,将Rift垂直同步(v-sync)与OS合成器分离,避免额外的GPU缓冲,使延迟降到最低,使用ovrHmd_AttachToWindow,窗口交换链输出被导向Rift,希望直接模式成为较长期的解决方案。

VR开发需要注意的事项:

  • 不要控制玩家的头部!
  • 注意第一人称动作。
  • 照片现实主义是没有必要的。
  • 不要使用电影级的渲染效果!比如可变焦距、过滤器、镜头光斑、泛光、胶片颗粒、暗角、景深等。

立体渲染质量检查:

  • 左右方向正确吗?
  • 双眼中的元素相同吗?
  • 两幅图像代表同一时间吗?
  • 刻度正确吗?
  • 深度一致吗?
  • 避免快速深度变化了吗?

良好的虚拟现实引擎必须满足以下条件:

  • 高质量的视觉效果。高质量视觉效果指没有任何东西会分散你的注意力,让你沉浸在游戏中,良好的着色效果(但不一定是真实照片),通常意味着良好的抗锯齿。为什么良好的抗锯齿至关重要?人类感知的本质意味着我们很容易被高频噪点分心,分心会降低存在感,使用立体渲染时,锯齿伪影可能会更严重,它们会导致视网膜竞争,良好的抗锯齿比原生分辨率更重要。抗锯齿方法有:边缘几何AA,通常硬件加速;图像空间AA,非常适合大多数渲染管线,如FXAA、MLAA、SMAA等;时间AA,使用再投影进行时间超采样。

  • 一致的高帧速率。为什么一致的高帧速率至关重要?在虚拟现实中,低帧速率看起来和感觉都很糟糕,如果没有高帧率,测试就很困难。在整个开发过程中保持高帧率,缺少V-sync也更为明显,因此,请确保启用了V-sync。

    在当前的引擎中,“通道”的概念被广泛接受,如反射渲染、阴影渲染、后处理等,每个通道都有不同的要求,每个通道都要找出瓶颈所在。CPU?DrawCall?状态设置?资源设置?GPU?顶点处理受限?几何处理受限?像素处理受限?

    绘制调用、状态设置或资源设置时CPU受限?考虑如何使用几何着色器,可以减少绘制调用的总数,阴影级联渲染:drawCallCount/n,其中n是层叠的数量,立方体贴图渲染:drawCallCount/6。降低资源设置成本,它还有其它特性可以帮助将处理从CPU上移开。

    几何渲染单元将一个图元流转换为另一个可能更大的图元流,在像素着色器之前发生,即在直接顶点像素绘制调用中的顶点着色器之后,如果启用了细分,则在Hull着色器之后。

    几何体着色器功能,渲染目标索引/视口索引,用于单程立方体贴图渲染、阴影级联、S3D、GS实例,允许逐图元运行同一几何体着色器的多次执行,而无需再次运行上一个着色器阶段。

    用于立体3D渲染的几何体着色器,一种使引擎立体3D兼容的简单方法,为每种材质添加一个GS(或调整已有材质的GS),如下所示:

    [maxvertexcount(3)]
    void main(
        inout TriangleStream<GS_OUTPUT> triangleStream,
        triangle GS_INPUT input[3])
    {
        for(uint i = 0; i < 3; ++i)
        {
            GS_OUTPUT output;
            output.position = (input[i].worldPosition , g_ViewProjectionMatrix);
            triangleStream.Append(output);
        }
    }

    顶点/几何体受限?通过压缩属性来减少顶点大小,在着色器阶段之间打包所有属性,如果正在使用用于放大或细分管线的几何体着色器,这一点很重要。考虑使用延迟获取(late fetch)法:将顶点属性数据绑定为使用的着色器阶段中的缓冲区,高度依赖硬件,始终调试性能,看看是否有影响!减少在GPU周围移动的数据。

    像素受限?降低像素着色器的复杂性,减少每帧着色的像素数,一个使用较小渲染目标的实验上采样与高质量视觉冲突,引入光晕、微光和视网膜冲突。

    考虑使用重新投影来加速立体3D渲染的方面,在PlayStation 3立体声3D游戏中获得巨大成功,然而,它只能在视差较小的情况下成功使用。

  • 出色的跟踪和标定。一般由SDK处理跟踪,使用SDK提供的跟踪矩阵。游戏定义的默认观看位置和方向:

    追踪玩家头部与摄像机的偏移量:

    玩家眼睛相对于头部矩阵的偏移量:

    跟踪器重置功能:设置头部位置,使其与游戏摄像机的位置和偏航对齐。重置位置和方向跟踪,重新调整游戏世界与现实世界的关系,以便固定的玩家位置,传递和游戏(Pass-and-play),匹配不同身高的玩家。

    跟踪允许用户接近跟踪体积中的任何内容,无法实现超昂贵的效果,并声称“这只是角落里的一个小东西”,即使是最低画质也需要比传统创作的更高的逼真度,如果在跟踪体积中,必须是高保真的。

  • 低延迟。为什么减少延迟如此重要?延迟是输入和响应之间的时间间隔,重要的不仅仅是始终如一的高帧率。不仅用于虚拟现实头部跟踪,提高响应能力在游戏中至关重要,游戏编程人员了解响应控制的必要性,网络程序员了解对响应性对手的需求等等。

假设现在拥有一个非常高效、高帧率、低延迟、超高质量的下一代引擎中拥有了出色的跟踪功能,该引擎针对虚拟现实进行了优化……引擎的工作完成了吗?当然不是!还有特定于平台的优化、跟踪外围设备、社交方面、游戏性/设计元素等工作。

Valve公司早在2014年就有多年的VR研究经验,联合了硬件和软件工程师,专为VR设计的定制化光学元件,显示技术——低持久性、全局显示,跟踪系统(基于基准的位置跟踪、基于点的桌面跟踪和控制器、激光跟踪HMD和控制器),SteamVR API–跨平台、OpenVR。

HTC Vive开发者版规格:刷新率是90赫兹(每帧11.11毫秒),低持久性,全局显示,帧缓冲区的分辨率是2160x1200(每只眼睛1080x1200),离屏渲染的宽高约1.4倍:每只眼睛1512x1680 = 254万个着色像素(蛮力),FOV约为110度,360⁰ 房间尺度跟踪,多个跟踪控制器和其它输入设备。

每秒着色可见像素数的估算:30赫兹时720p:2700万像素/秒,60Hz时1080p:1.24亿像素/秒,30英寸监视器2560x1600@60赫兹:2.45亿像素/秒,4k监视器4096x2160@30赫兹:2.65亿像素/秒,90赫兹时的VR 1512x1680x2:4.57亿像素/秒,可以将其降低到3.78亿像素/秒,相当于非虚拟现实渲染器在100赫兹时的30英寸监视器。

最低化GPU最低规格,最低规格越低,客户就越多,客户不应注意到锯齿,客户将锯齿称为“闪烁”,算法应该扩展到多个GPU上。

桌面VR可以尝试立体渲染(多GPU),AMD和NVIDIA都提供DX11扩展以加速跨多个GPU的立体渲染,AMD实现的帧速率几乎翻了一番,但还没有测试NVIDIA的实现。非常适合开发人员,团队中的每个人都可以在他们的开发盒中使用多GPU解决方案,在没有不舒服的低帧率的情况下打破帧率。

VR的交互技术包含选择、操纵、导航、系统控制等方面。三维选择包含从集合中拾取一个或多个对象、现实世界的隐喻(触摸/抓取、定点)、“自然”技术(简单虚拟手、射线投射)等。3D选择的影响因素有:技术(跟踪抖动、精度、延迟)、人类(手抖动)、环境(距离、遮挡)等,半天然的“天然”技术,即使是完全自然的技术也不是最佳的。可以使用双气泡(Double Bubble):扩展光线投射,动态体积光标、渐进式优化。3D选择技术的应用场景如下表:

相比Ray Cast,Double Bubble在选择时间、误差方面表现更好:

使用真实世界的隐喻,技术和现实世界的限制,打破现实世界的假设:

每种操作方式在各个阶段的描述如下:

3D交互的最后的想法是自然主义vs 魔法(超自然、超级自然)、与不精确工具的精确交互(渐进式优化、动态C/D增益、虚拟摩擦力):

虚拟实体的影响也比较关键,许多关于化身影响的研究,对社交互动至关重要,对于存在(对某些人)也至关重要。

在手势和身体方面,需要手势向他人解释,有些人经常做手势,语言学研究人员研究了手势对解释困难概念能力的影响。

从左到右:无化身(avatar)、有化身但无移动、完整的化身和移动。

拥有化身显著提高了执行对象记忆任务的能力,有化身的人比没有化身的人做更多的手势。延迟至关重要,较低的延迟倾向于更“自然”的接口,但在所有情况下,这些接口可能不是最有效的接口。虚拟身体对某些用户非常重要,“虚拟现实”研究可以在众多学科中找到,因为其影响和需求非常广泛,一个非常多样化的研究社区促成了令人兴奋和有趣的研究合作。

在现实物理空间和虚拟现实的空间映射中,虚拟现实的物理定律是可变的,人类的感知是可塑的,我们可以利用它来提高可用性,可以创造超现实、神奇的体验。

XR通常存在空间感知技术,运动跟踪-深度感应-区域学习,表面重建–平面和孔洞检测。

空间感知的相关设备:

对于Microsoft的HoloLens,采用了红外相机空间映射:

支持运动和手势跟踪:

语音识别,包含系统级命令、用户可配置命令。

空间处理HoloToolkit支持基本的空间映射(访问/可视化空间数据,保存/加载房间)和空间处理(曲面网格到平面,墙、天花板、地板、桌子,未知,地板缓冲器,天花板缓冲器,自定义形状定义)。

Tango运动追踪支持视觉惯性里程计(VIO,跟踪图像差异,惯性运动传感器,组合以提高精度),限制是漂移、无内存、照明等。2016年的Tango和HoloLens的对比如下:

2017年的VR游戏Climb采用了严密的计划,成功解决了新平台问题,运动方面取得突破,使用保守的技术方法,设计驱动的功能有时会出现问题。

Robinson分析性能和内存,平台工具运行良好,艺术团队成功采用程序可视化分析工具,在屏幕上的OOM崩溃跟踪内存,新功能可分析当天保存的峰值。

注释点(透镜匹配)渲染上,利用PS4近/宽渲染支持,对于每只眼睛,渲染内部和外部视图。

大大有助于在性能和分辨率之间找到最佳点,PS4渲染的内部面积等于1.5倍渲染比例(1620p),PS4 Pro将其增加到1.9倍,更大的内径

外环在PS4上采样不足,Pro 1:1,需要渲染场景四次。在渲染线程上录制场景drawcalls的成本高出四倍,为场景和照明重新提交相同的命令缓冲区:

后处理仍录制4次,有些数据需要修补,每次提交后覆盖现有的每视图常量缓冲区在每次提交后复制后处理(对象速度)期间所需的渲染目标。为了节省GPU成本,顶点着色器执行了4次,但开销可以接受,通过将Post交错作为异步作业来吸收GBuffer中的顶点开销,填充HTILE掩码以拒绝相关区域之外的像素。

总之,Robinson的计划/时间表不稳定,预留空间给开发方(性能、内容、游戏性),移动和用户选项的结果参差不齐,技术创新高度成功,媒体/平台上凸起的可视栏。人工移动已经存在并将继续存在,用户界面/用户体验还有很长的路要走,VR性能并不难,峰值可以

在主流硬件上实现高保真,到目前为止,只是触及表面。下图是VR系统场景的组件:

输入处理器、模拟处理器、渲染处理器和世界数据库关系如下:

VR分类可以基于两个因素:使用的技术类型和精神沉浸程度,具体如下图:

解决未来关键的XR技术挑战包含显示、照明、运动追踪、电量和散热、连接等。

15.2.1.1 软件架构

VR应用常涉及实现所有事情!如硬件故障,需要支持一切(太古代),从SDK提取输入,管理SDK,UI框架等。其中的一种VR分层架构如下:

  • 特定于SDK的输入类:每个硬件/SDK一个类,没有特定于项目的逻辑!监听设备输入,调用抽象处理程序,调用工具的down/hold/up,调用常规输入的down/hold/up。

  • 输入组件:SDK特定组件类包含对硬件功能的特定引用:

    public class ViveControllerComponents : WandComponents
    {
        public SteamVR_Controller.Device viveController;
    }

    特定于类别的组件类包含硬件类型的公共属性:

    public class WandComponents : InputComponents
    {
        public Transform handTrans;
        public override Vector3 Position { get { return handTrans.position; } }
        public override Vector3 Forward { get { return handTrans.forward; } }
        public override Quaternion Rotation {get{ return handTrans.rotation; }}
    }

    InputComponents基类包含大多数抽象数据:

    public class InputComponents
    {
        public virtual bool Valid { get { return true; } }
        public virtual Vector3 Position { get { return Vector3.zero; } }
        public virtual Vector3 Forward { get { return Vector3.forward; } }
        public virtual Quaternion Rotation { get { return Quaternion.identity; } }
    }
  • 工具基类。

    // 每个SDK的向下/保持/向上挂钩
    public virtual void DoToolDown_Sixense(SixenseComponents sxComponents)
    {
        DoToolDown_Wand(sxComponents);
    }
    public virtual void DoToolDown_Leap(LeapComponents leapComponents)
    {
        DoToolDown_Optical(leapComponents);
    }
    public virtual void DoToolDown_Tango(TangoComponents tangoComponents) {
        DoToolDown_PointCloud(tangoComponents);
    }
    
    // 每个类别的向下/保持/向上挂钩
    public virtual void DoToolDown_Wand(WandComponents wandComponents)
    {
        DoToolDown_Core(wandComponents);
    }
    public virtual void DoToolDown_Optical(OpticalComponents opticalComps)
    {
        DoToolDown_Core(opticalComps);
    }
    public virtual void DoToolDown_PointCloud(PCComponents pcComponents)
    {
        DoToolDown_Core(pcComponents);
    }
    
    // 工具基础函数
    DoToolDown_Core
    DoToolDownAndHit*
    DoToolHeld_Core
    DoToolUp_Core
    DoToolDisplay_Core
    
    public virtual void DoToolDown_Core(InputComponents comp)
    {
        if (Physics.Raycast(comp.Position, comp.Forward, out hit, dist, layers))
        {
            DoToolDownAndHit(comp);
        }
    }
  • 特定工具类型。

    // 可以覆盖DoToolDown_Core等,实现完全平台无关逻辑
    public class MoveTool : Tool
    {
        protected override void DoToolHeldAndHit(InputComps comps)
        {
            selectedTrans.position = hit.point;
        }
    }
    
    // 可以覆盖任何类别或特定于SDK的挂钩,以实现更定制的行为
    public class MoveTool : Tool
    {
        // ...
        protected override void DoToolHeld_Optical(OpticalComps comps)
        {
            // Move mechanic that’s more appropriate for optical control
        }
    }
  • 硬件输入基类。非工具抽象输入,适用于一般游戏功能和一次性交互,三种处理方法:

    // 比如Unity的原生输入类
    bool HardwareInput.ButtonADown/Held/Up
    // 当想要那个观察者的时
    event HardwareInput.OnButtonADown
    // 集中输入/游戏逻辑
    void HardwareInput.HandleButtonADown()
  • 游戏性/一般输入类…。

    // 一次性输入
    public class GameplayController : MonoBehaviour
    {
        void Update()
        {
            if(HardwareInput.TriggerDown)
            {
                WorldConsole.Log("Fire ze missiles!");
            }
        }
    void Awake()
    {
        HardwareInput.OnButtonADown += HandleButtonA;
    }
    
    void HandleButtonA()
    {
        WorldConsole.Log("Boom!"); // Btw: use a “world console”!
    }
    } // 组件的一般输入, 更新的三种方法: // 添加硬件输入/位置/向前等 HardwareInput.ButtonADown/Held/Up // 传递包含组件的事件参数 HardwareInput.OnButtonADown(args) HandleButtonADown(components)

所有SDK都以Libs/dir的形式存在于项目中:

SDK太多了!SDK之间的AndroidManifest和插件冲突,在某些情况下,可以通过合并清单来解决(例如Cardboard+Nod),在许多情况下,只需要将冲突的SDK移入或移出Asset文件夹,可以连接到构建管线中。对于多SDK场景设置,将场景设置为支持所有SDK,Player对象包含用于ViveInput、GamepadInput、CardboardInput、NodeInput、LeapInput、TangoInput的组件…好处是场景之间没有重复的工作,所有设备都可以同时启用(例如Vive+Leap)。好处是让多种设备类型交互意味着新的设计挑战,更多平台==更复杂的场景,可以将播放器拆分为更易于管理的预置体,并在运行时或使用编辑器脚本组装这些预置。对于SDK管理器编辑器脚本,在编辑器中或在构建时启用/禁用每个平台的组件和对象。

public void SetupForCardboard()
{
    Setup(
        // Build settings
        bundleIdentifier: "io.archean.cardboard",
        vrSupported: false,
        // GameObjects
        cameraMasterActive: true,
        sixenseContainerActive: false,
        // MonoBehaviours
        cardboardInputEnabled: true
    );
}

此外,可以自定义输入模块将允许向uGUI添加新的硬件支持。块状和点击用户界面(Block-and-pointer UI),点击或按下时,<设备>将光线投射输入用户界面(例如Vive),或简单的碰撞(如Leap)。自定义按钮组件:

ButtonHandler类中有一个巨大的switch语句来映射所有动作:

switch(button.action)
{
    case ButtonStrings.Action_TogglePalette: TogglePalette(button, state); break;
    case ButtonStrings.Action_ChangePage: ChangePage(button, state); break;
    case ButtonStrings.Action_ChangePagination:ChangePagination(button); break;
    case ButtonStrings.Action_SelectProp: SelectProp(button, state); break;
    case ButtonStrings.Action_SelectTool: Tool.HandleSelectToolButton(button); break;
    …

还有一个文件,里面有用于操作的常量字符串:

public const string Action_TogglePalette = "togglePalette";
public const string Action_ChangePage = "changePage";
public const string Action_ChangePagination = "changePagination";
public const string Action_SelectProp = "propSelect";
public const string Action_SelectTool = "toolSelect";
....

通过参数字段可以获得更高级和可重用的功能,用户界面代码集中,按钮可以传递任何数据类型,非常方便。所以你想做一个多平台的虚拟现实应用,SteamVR&Cardboard加入Unity的原生VR支持,从一开始就应该计划多平台。

虚拟现实系统由硬件和软件两个主要子系统组成,硬件可进一步分为计算机或VR引擎和I/O设备,而软件可分为应用软件和数据库,如下所示。

下图显示了名为CalVR的VR框架的不同模块,CalVR本身构建在OSG之上,而OSG又构建在OpenGL之上。菜单API目前支持两个菜单小部件库:Board菜单和Bubble菜单。CalVR使用一组设备驱动程序,例如Kinect或Ring鼠标,它允许运行自定义插件。

下图是VR系统的第三人称透视图。假设工程硬件和软件是完整的VR系统是错误的:有机体及其与硬件的交互同样重要。此外,在VR体验过程中,与周围物理世界的交互不断发生。

缓冲通常用于视觉渲染管线中,以避免撕裂和丢失帧;然而,它引入了更多的延迟,对VR不利。

15.2.1.2 Quest 2开发

Quest 2是Oculus于2020年发行的一款VR一体机,使用了高通Snapdragon XR2芯片组,其中Snapdragon XR2芯片组的硬件基本参数如下:

CPU

Octa-core Kryo 585 (1 x 2.84 GHz, 3 x 2.42 GHz, 4 x 1.8 GHz)

GPU

Adreno 650

在Quest 2开发互动应用,一种可行的绘制调用预算是:每个网格/对象1个调用,该对象上的每个唯一材质(或材质实例)调用1次,限制网格上材质的数量,Atlas纹理可减少材质数量,可以合并网格的位置。

可以使用RenderDoc与任务的连接,捕获任务绘制的帧,需要参考draw调用的总数,单步执行单个绘制调用,发现性能方面的潜在问题。

OVRMetrics/FPS计数器:FPS是最重要的性能指标!理想情况下保持在72,但至少在65以上,使用FPS计数器查看运行时的帧速率。

使用前向渲染,无深度渲染,单通道立体渲染,消除锯齿,注视点渲染。

在UE和Unity中设置前向渲染。

在UE和Unity中设置单通道立体渲染。

Oculus Quest支持固定注视点渲染(Fixed Foveated Rendering,FFR)。FFR允许以低于眼睛缓冲区中心部分的分辨率渲染眼睛缓冲区的边缘。请注意,与其它形式的注视点技术不同,FFR不基于眼睛跟踪,高分辨率像素“固定”在眼睛缓冲区的中心。使用FFR的视觉效果几乎难以察觉,但FFR的性能优势包括:

  • 显著提高GPU填充性能。
  • 降低功耗,从而减少热量并延长电池寿命。
  • 使应用程序能够提高眼睛纹理的分辨率,从而改善观看体验,同时保持性能和功耗水平。

使用FFR时有一些权衡:

  • FFR对于低对比度纹理(包括背景图像和大型对象)最有用。
  • FFR对于高对比度项目(如文本和精细详细的图像)不太有用,并且会导致图像质量明显下降。
  • 复杂片段着色器受益于FFR。

可以逐帧调整FFR级别,以便在性能和视觉质量之间实现最佳权衡。通常,应该尽可能多地使用FFR,并将其设置为尽可能高的级别,但应该测试内容并查找任何不需要的视觉瑕疵。因为应该尽量使用FFR,所以建议使用动态FFR,根据GPU负载和应用程序的要求自动设置FFR级别。

FFR提供的增益(或损失)通常取决于应用程序的像素着色器成本。FFR可使像素密集型应用程序的性能提高25%。另一方面,使用非常简单的着色器(未绑定到GPU填充)的应用程序可能不会看到FFR的显著改进。高度ALU绑定的应用程序将从中受益,如下图所示,它在场景中收集GPU百分比。鉴于16%的GPU利用率来自timewarp(因此不受FFR的影响),此图显示的性能比低设置提高了6.5%,比中设置提高了11.5%,比高设置提高了21%。

这显示了使用FFR的最佳情况。如果在具有非常简单的像素着色器的应用程序上执行相同的测试,则实际上可能会在低设置上产生净损失,因为使用FFR的固定开销可能高于在相对较少的几个像素上的渲染节省。事实上,在这种情况下,可能会体验到高设置的轻微增益,但它不值得图像质量损失。与传统的2D屏幕不同,VR设备要求向观众显示的图像扭曲,以匹配HMD中镜头的曲率。这种扭曲使我们能够感知到一个更大的视野,而不仅仅是简单地看一个原始的显示器。下图显示了扭曲的效果,其中2D平面(水平线)扭曲成球形:

由于扭曲,构成眼睛纹理的像素的表示非常不均匀。在FOV边缘创建后扭曲区域需要比FOV中心更多的像素,这导致FOV边缘的像素密度高于中间的。由于用户通常会朝屏幕中央看,会产生很大的反作用。最重要的是,镜头会模糊视野的边缘,因此即使在眼睛纹理的这一部分渲染了许多像素,图像的清晰度也会丢失。GPU花费大量时间渲染FOV边缘无法清晰看到的像素,是非常低效的。

注视点渲染通过在计算期间降低输出图像的分辨率来回收一些浪费的GPU处理资源,它是通过控制GPU上各个渲染分片的分辨率来实现的。Oculus Quest使用分块(tile)渲染器,FFR的工作原理是控制各个分块的分辨率,并确保落在眼睛缓冲区边缘的分块的分辨率低于中心,从而减少了GPU需要填充的像素数量,而不会明显降低后扭曲(post-distortion)图像的质量,因此,对于渲染大量像素的应用程序,GPU性能有了非常显著的改善。

下面的屏幕截图显示了1024x1024眼缓冲区的分块分辨率倍增图。这些颜色表示以下示例图像中的以下分辨率级别,以演示FFR设置:

  • 白色=全分辨率:这是FOV的中心,纹理的每个像素都由GPU独立计算。
  • 红色=1/2分辨率:GPU仅计算一半像素。当GPU将其计算结果存储在通用内存中时,将在解析时从计算的像素中插值缺失的像素。
  • 绿色=1/4分辨率:GPU仅计算四分之一的像素。当GPU将其计算结果存储在通用内存中时,将在解析时从计算的像素中插值缺失的像素。
  • 蓝色=1/8分辨率:GPU仅计算八分之一的像素。当GPU将其计算结果存储在通用内存中时,将在解析时从计算的像素中插值缺失的像素。
  • 粉红色=1/16分辨率:GPU仅计算十六分之一的像素。当GPU将其计算结果存储在通用内存中时,将在解析时从计算的像素中插值缺失的像素。

Quest支持动态注视点功能,可以配置注视点级别,通过启用动态注视点,根据GPU利用率自动调整。启用动态注视点时,注视点级别将自动调整,指定的注视点级别为最大值。根据GPU的利用率和应用程序的要求,系统会上升到所选的注视点级别,但决不会超过该级别。尽量使用动态FFR,而不是Unreal的动态分辨率功能。有多种方法可以设置FFR级别并启用动态注视点:

  • 项目设置。可以在Unreal项目设置中的OculusVR插件页面设置FFR级别。

  • API设置。可以使用以下方法将FFR级别设置为以下任何索引:

    void UOculusFunctionLibrary::SetFixedFoveatedRenderingLevel(EFixedFoveatedRenderingLevel level, bool isDynamic)
  • 蓝图设置。通过以下蓝图节点获取和设置FFR级别:

Multi-View是基于Android的Oculus平台的高级渲染功能。如果应用程序受到CPU的限制,强烈建议使用多视图来提高性能。在典型的立体渲染中,必须按顺序渲染每个眼睛缓冲区,从而使应用程序和驱动程序开销加倍。启用“多视图”后,对象将渲染一次到左眼缓冲区,然后自动复制到右眼缓冲区,并对顶点位置和视图相关变量(如反射)进行适当修改。OpenGL和Vulkan API支持多视图渲染。若要开启Multi-View,打开虚幻的设置页面:Edit > Project Settings > Engine > Rendering,勾选以下选项:

相位同步(Phase Sync)是一种用于自适应管理延迟的帧定时管理技术,它可作为UE4.23及更高版本中的一个选项用于Quest和Quest 2应用程序。Phase Sync为Oculus Quest和Quest 2应用程序提供了一种替代传统固定延迟模式的方法来管理帧计时。固定延迟模式意味着尽可能早地合成帧,以避免丢失当前帧和需要重用过时帧,过时帧会对用户体验产生负面影响。与固定延迟不同,相位同步根据应用程序的工作负载自适应地处理帧定时。相位同步的目标是在合成器需要完成的帧之前进行帧完成渲染,可以减少渲染延迟,而不会丢失帧。针对Quest和Quest 2的应用程序应启用相位同步提供的自适应帧定时。请注意,Quest 2的CPU和GPU资源比Quest多,并且可能会过早渲染帧,从而增加延迟,而相位同步有助于减少此延迟。下图显示了典型多线程VR应用程序的固定延迟与启用相位同步之间的差异。

启用相位同步时,请注意以下事项:

  • 没有额外的性能开销。
  • 如果应用程序的工作负载剧烈波动或频繁出现峰值,则相位同步可能会导致比未启用相位同步时使用更陈旧的帧。
  • 延迟锁(Late-Latching)和相位同步通常是相辅相成的。
  • 如果额外延迟模式和相位同步都已启用,则将忽略额外延迟模式。

要在虚幻引擎中启用相位同步,打开Edit > Project Settings > Plugins > OculusVR,在Mobile部分,选中Phase Sync复选框。

测试相位同步:在应用程序中启用阶段同步后,可以通过检查logcat日志来验证它是否处于活动状态,并查看它节省了多少延迟。

adb logcat -s VrApi

如果相位同步未激活,Lat值为Lat=0或Lat=1,表示额外延迟模式。如果相位同步处于活动状态,则Lat值为Lat=-1,表示延迟是动态管理的。

Prd值指示由运行时测量的渲染延迟。要计算相位同步节省了多少延迟,请比较相位同步处于活动状态和未处于活动状态时的Prd值。例如,如果有相位同步的Prd为35ms,没有相位同步的Prd为45ms,则使用相位同步可节省10ms的延迟。为了更容易地比较有无相位同步的性能,可以使用adb shell setprop打开和关闭相位同步。更改setprop后,必须重新启动应用程序,更改才能生效。

  • 关闭:adb shell setprop debug.oculus.phaseSync 0.
  • 打开:adb shell setprop debug.oculus.phaseSync 1.

可以在Quest上的Unreal Engine中使用某些色调映射效果,而不会产生与色调映射相关的传统性能成本。Oculus集成可以用最少600微秒的额外渲染时间渲染色调映射,因为它使用Vulkan subpass而不是Unreal Engine的移动HDR模式或额外的渲染通道。此功能仅在使用Vulkan的UE 4.26的Oculus分支中可用。具体详情可参阅Tone Mapping in Unreal Engine

Quest在UE中支持VR合成器层(VR Compositor Layers)。使用Unreal,可以将透明或不透明的四边形、立方体贴图或圆柱形覆盖层添加到级别,作为合成器层。异步时间扭曲合成器层(例如世界锁定覆盖)以与合成器相同的帧速率渲染,而不是以应用程序帧速率渲染。它们不太容易抖动,并且通过镜头进行光线跟踪,从而提高了其上显示的纹理的清晰度。

建议对文本使用合成器层,在合成器层上渲染的文本更清晰。另外,凝视光标和UI很适合渲染为四边形合成器层。圆柱体对于平滑曲线UI界面可能很有用,立方体贴图可用于启动场景或Skybox。建议在加载场景中使用立方体贴图合成器层,这样即使应用程序不执行任何更新,它也将始终以稳定的最小帧速率显示,可以显著缩短应用程序启动时间。

在4.13及更高版本的Unreal中支持四边形、圆柱体和立方体贴图层。默认情况下,VR合成器层始终显示在场景中所有其它对象的顶部。可以通过启用“支持深度”(Supports depth),将合成器层设置为响应深度定位。如果使用多个图层,请使用优先级设置控制图层显示的深度顺序,较低的值表示优先级较高(例如,0在1之前)。请注意,启用Supports depth度可能会影响性能,因此请谨慎使用,并确保评估其影响。

要创建一个overlay,请执行以下操作:

  • 创建一个Pawn并将其添加到关卡。可以使用UMG UI设计器向Pawn添加任何所需的UI元素。
  • 选择Pawn,选择Add Component,然后选择Stereo Layer
  • 在“Stereo Layer options”下,将“Stereo Layer Type”设置为“Quad Layer”、“Cylinder Layer”或“Equirect Layer”。
  • 将“Stereo Layer Type”设定为“Face Locked”、“Torso Locked”或“World Locked”。
  • Quad Stereo Layer PropertiesCylinder Stereo Layer Properties中以世界单位设置overlay尺寸。
  • 选择“支持立体层中的深度”(Supports Depth in Stereo Layer)可将合成器层设置为不总是显示在其他场景几何体的顶部。请注意,此设置可能会影响性能。
  • 根据需要配置纹理和其它属性。
  • 选中“双三次过滤”复选框,启用为Quest显示调整的GPU硬件双三次过滤,以在呈现VR图像时享受额外的保真度。
    • 注意:随着内核占用空间的增加,双三次过滤需要更多的GPU资源,对于三线性缩小尤其如此,因为它需要从单独的mip级别进行两次双三次计算。如果直接用于合成器层,增加的GPU成本将在合成计时中表现出来,可能会导致帧下降并对VR体验产生负面影响。应该权衡增加的视觉保真度与提供最佳VR用户体验所需的额外GPU资源。

将组件从属于的Pawn将固定在四边形或圆柱体的中心。最多可以将三个VR合成器层添加到移动应用程序,最多可以将十五个VR合成器层添加到Rift应用程序。

光影方面,烘焙灯光以获得更高性能的灯光效果,但请记住,烘焙光照贴图需要时间,根据团队规模和环境数量平衡时间。一次仅一个动态灯光,无论如何,还是要考虑在静态区域烘焙。动态阴影非常昂贵,一次只能投射一个阴影灯光,仅硬阴影,尽可能避免阴影投射,除非游戏渲染非常轻量级。无照明(Unlit)着色器的性能非常好,消除花费在照明和烘焙光照贴图上的时间,总体上需要较少的纹理。照明,但使用卡通阴影作为替代方案,在不完全不照明的情况下计算更少。

保持低纹理分辨率,使用尽可能少的图集,也许可以尝试在没有特定贴图的情况下进行,或者打包到RGBA通道中,尽可能重复使用和平铺,别忘了mip!指令数影响性能,纹理的数量会影响性能,尤其是当它们在屏幕上平铺时,但是着色器也可以真正有助于创建美丽和独特的外观。尝试使用轻量级着色器,看看它们能做什么意外的工作。切换材质和着色器对每次绘制调用的性能有轻微影响,Atlas尽可能减少独特材质的数量,即使它不会减少绘制调用。如果可以,请合并着色器以限制唯一着色器的数量。

仅在内存中实例化不会减少绘制调用,某些类型的实例执行合并/批处理绘制调用,LOD仍然存在,并且仍然有效!某些类型的实例还使用LOD和批处理绘制调用。使用良好的行业惯例,尤其是在从高保真到低保真的情况下。探索新风格!将有用且适用的低多边形样式集成到游戏中,作为难题的解决方案,示例:如果在使用传统方法时遇到性能问题,请查看低多边形样式中如何处理树木或树叶。

可以使用批处理,在一次调用中绘制多个对象!不同引擎有不同的类型和方法,但谨记批处理都有一点开销。找出批量或使用其他解决方案(如合并)是否更便宜。在Unity中,有动态批处理(300顶点以下相同材质的相同网格,一些开销,但对于小的重复对象非常好)和静态批处理(更高的多边形模型,但使用更多内存)。在UE中有实例化静态网格(Instanced Static Meshes,一次draw调用,但在其他方面不会节省太多性能)和层次实例化静态网格(Hierarchical Instanced Static Meshes,LOD和裁剪可生效,但很难使用,所以需要制作一个工具来帮助使用者)。

对于半透明,具有透明度的小对象的性能非常好,重叠的大型透明对象对性能影响最大,尽量少使用透明度,在设备上充分测试半透明!!

左:软Alpha卡片效果多少存在一些问题;右:顶点雾是一种仍然有效的旧方法。

减少半透明的空白像素区域,可有效提升性能。

通过过渡获得创意!不是所有的东西都需要Fade。

对于后处理,颜色校正可以在着色器中完成,如果真的需要在一些地方bloom,可以用一些卡片来伪装它。可能无法使用景深、屏幕叠加(screen overlay)和奇特的后期着色器。思考需要从后处理中获得什么,并尝试以其它方式实现。

15.2.1.3 OpenXR

OpenXR是Kronos出的XR标准API,OpenXR提供跨平台、高性能的访问,可跨多个平台直接访问XR设备运行时。

典型的OpenXR应用程序的高级概述,包括函数调用顺序、对象创建、会话状态更改和渲染循环(下图)。

更多OpenXR的介绍参考官网:https://khronos.org/openxr。

所有镜头都会引入图像扭曲、色差和其它失真,我们需要在软件中尽可能地对其进行校正!通过HMD镜头看到的栅格,可发现图像的横向(xy)扭曲和色差:扭曲取决于波长!

图像扭曲(挤压变形)的两种形式:透镜畸变和筒体变形:

其中上图左是由光学部件(镜头)引起的,而上图右是应用程序为了抗光学畸变而有意为之。整体工作原理如下:

可调节的眼镜佩戴者无需调整即可适应瞳孔间距的变化:

下图则是关键的(上)和可容忍的(下)参数示意图:

对于立体3D而言,在摄影立体和头盔显示器的固定设置下的所需的焦距要求:

结合下图,(a)大多数HMD的视野都很窄,(b)实现宽视场需要更高分辨率的显示器,(c)或更大的像素。(d)如何做到两全其美?利用眼睛的可变敏锐度,(e)使用扭曲着色器压缩图像的边缘,(f)光学元件应用反向失真,使边缘看起来再次正确,(g)中心像素较小,边缘像素较大。

光学与变形:Warp通道分别为RGB使用3组UV,以考虑空间和颜色失真。

可视化1.4倍的渲染目标。其中上图是扭曲前,下图是扭曲后。

模板网格(隐藏区域网格):用模板屏蔽掉实际上无法透过镜头看到的像素,GPU在提前模板拒绝时速度很快。或者,可以渲染到接近z的深度缓冲区,以便所有像素都可启用提前z测试,透镜会产生径向对称变形,意味着可以有效地看到投影在面板上的圆形区域。

模板网格图例。从上到下从左到右依次是:扭曲视图、理想扭曲视图、浪费的空间、无扭曲视图、无扭曲视图(屏蔽无效像素)、最终无扭曲视图、最终无扭曲的分离视图。

模板网格(隐藏区域网格):SteamVR/OpenVR API提供此网格,填充率可以降低17%!无模板网格:VR 1512x1680x2@90Hz:4.57亿像素/秒,每只眼睛254万像素(总计508万像素),带模板网格:VR 1512x1680x2@90Hz:3.78亿像素/秒,每只眼睛约210万像素(总计420万像素)。

扭曲网格,依次是:镜头畸变网格、暴力、剔除0-1之外的UV、剔除模板网格、收缩扭曲。

VR还涉及透镜畸变和像差校正(aberration correction):

下图是Oculus Rift的透镜结构和原理:

人类视觉颜色系统如下图,人眼无法测量,大脑无法测量每个波长的光,相反,眼睛测量三个响应值=(S、M、L),根据S、M、L锥的响应函数。

人类的单目和双目视野如下图,每只眼睛约160°视野(总视野约200°,注:不考虑眼睛在眼窝中旋转的能力)。

具有人眼视力的VR显示器:

考虑有限的VR显示刷新率:

情况2:相对于眼睛移动的对象:

案例3:眼睛移动以跟踪移动对象:

提高帧速率可以减少抖动,较高的帧速率(下图最右侧的图表),更接近地面真相:

减少抖动:低持久性显示。低持久性显示:像素在小部分帧中发光,Oculus DK2 OLED低持久性显示器:75 Hz帧速率=每帧约13 ms,像素持久性=2-3毫秒。

VR中的延迟要求具有挑战性,VR图形系统的目标是实现“存在”,将大脑诱使成,认为它所看到的是真实的,实现存在需要极低延迟的系统。当你移动头时,你看到的必须改变!端到端延迟:从头部移动到新光子到达眼睛的时间。测量用户头部运动,更新场景/摄像机位置,渲染新图像,将图像传送至HMD,然后传送至HMD中的显示器,实际从显示器发出光(光子击中用户的眼睛)。VR的延迟目标:10-25 ms,需要极低延迟的头部跟踪,需要极低延迟的渲染和显示。

考虑1000 x 1000跨100°视野的显示器,每度10像素。假设在1秒内将头部移动90°(仅以中等速度),系统的端到端延迟为50毫秒(1/20秒),结果是显示的像素与理想系统中的像素相差4.5°~45像素,延迟为0。减少VR的延迟不仅对对抗负面影响(眩晕等)很重要,而且因为延迟对行动的执行至关重要,据称18ms的延迟很难察觉,但它仍然会影响性能,如果要优化3D交互,则必须分析延迟,否则结果可能无法传输。挑战在于低延迟和高分辨率需要较高的渲染速度,而VR设备往往渲染性能较低。

菲茨定律(Fitts's Law):在简单的定点任务上模拟人的运动,100篇学术论文(选择任何你喜欢的移动设备),20世纪90年代和2000年代的大多数VR结果处理的延迟为30ms-200ms。“性能”峰值约为30ms,低于30ms更“自然”,但速度较慢(在此任务中),假设运动系统具有“潜伏期”:

VR的延迟亦即Motion-to-photon的延迟,涉及多阶段:动作、传感器、处理与合并、渲染、Scanout、传输、像素变化时间、像素余辉。

将延迟保持在低值是提供良好虚拟现实体验的关键,目标是< 20毫秒,希望接近5毫秒。延迟减少方法概述将以下策略结合使用,以减少延迟并将任何剩余延迟的副作用降至最低:

  • 降低虚拟世界的复杂性。
  • 提高渲染管线性能。
  • 移除从渲染图像到切换像素的路径上的延迟。
  • 使用预测来估计未来的观察点和世界的状态。
  • 移动或扭曲渲染图像,以补偿最后一刻的视点错误和丢失的帧。

以上几个方法在书籍VIRTUAL REALITY by Steven M. LaValle中的章节7.4 Improving Latency and Frame Rates有详细阐述,感兴趣的同学不妨仔细阅读。

渲染延迟- 时间扭曲(TimeWarp):将渲染重新延迟到后面一个时间点,与变形同时进行,减少感受到的延迟,负责DK2滚动快门,SDK(如Oculus VR SDK)可以处理方向、位置。在帧结束前,使用传感器是否有其它方式?时间扭曲– 预测的渲染(John首创)。

从引擎的角度来看,减少延迟的一种方法是使用延迟上下文在多个线程上异步构建命令列表(又称命令缓冲区),作为即时上下文,在命令缓冲区中将命令排队时会产生渲染开销,相比之下,在回放期间,命令列表的执行效率要高得多,适用于“通道”的概念。多上下文渲染允许GPU在帧中更早地开始处理,从而减少延迟。

单上下文(上)和多上下文(下)渲染的对比。

为每只眼睛的视图并行创建和提交命令列表,立即减少CPU帧时间,如果引擎受CPU限制,意味着帧延迟会立即减少。如果引擎是GPU受限,但GPU因为启动得更早,故而在帧中完成得更早,也意味着帧延迟立即减少。是否有任何特定于虚拟现实的方法来应对延迟?采样跟踪数据和使用该数据渲染帧之间的时间需要尽可能短,不要使用超过两倍的缓冲,使用最新的方向数据重新投影图像可以改善明显的延迟和帧速率。尽可能降低被跟踪外围设备的延迟,是否有任何特定于平台的方法来应对延迟?

如果仍受CPU限制,也许Compute可以帮助将可并行任务卸载到GPU上。如果仍然受限于GPU,Compute允许我们从不同的、更通用的角度来思考GPU任务,在GPU未被充分利用的地方使用它,阴影渲染通常需要顶点/几何体,因此它是安排异步计算任务的好地方。

15.2.3.1 Prediction

带有Project Morpheus的PlayStation 4是一个已知的系统,硬件中存在任何延迟,库/软件中存在任何延迟,需要想方设法减少这些延迟。提供CPU和GPU性能分析工具,使开发者能够计算并减少游戏中的延迟,可以用它来预测图像显示时HMU的位置。减少引擎延迟是关键,但使用预测来掩盖任何微小的剩余延迟都可以很好地发挥作用,指定的预测量越小,其质量越好。

目标是尽可能缩短HMD和控制器变换的预测时间(渲染为光子)(精度比总时间更重要),低持久性全局显示:在11.11毫秒帧中,面板仅点亮约2毫秒。

上面的图像不是最佳的VR渲染,但有助于描述预测。

管线架构:渲染当前帧时模拟下一帧:

在提交之前,会重新预测转换并更新全局cbuffer,由于预测限制,虚拟现实实际上需要这样做,必须保守地在CPU上减少大约5度。

等待VSync:最简单的VR实现,在VSync之后立即预测,模式#1:Present(),清除后缓冲区,读取像素;模式#2:Present(),清除后缓冲区,在查询上自旋转(spin)。非常适合初始实现,但避免这样做,GPU不是为此而设计的。

“运行开始”的VSync:怎么知道离VSync有多远?很棘手,图形API并不直接提供这一点。Windows上的SteamVR/OpenVRAPI在一个单独的进程中,在调用IDXGIOutput::WaitForVBlank()时旋转,记录时间并递增一个帧计数器。然后,应用程序可以调用getTimeSincellastVsync(),该函数也会返回一个帧ID。GPU供应商、HMD设备和渲染API应该提供这一点。

“运行开始”的细节:要处理坏帧,需要与GPU部分同步,在清除后缓冲区后注入一个查询,提交整个帧,在该查询上旋转,然后调用Present(),确保在当前帧的VSync的正确一侧,现在可以旋转直到运行开始时间:

为什么查询题很关键?如果有一帧延迟,查询将在下一帧的VSync右侧,确保预测保持准确(下图橙色部分):

开始运行总结:具有一个稳定的1.5-2.0毫秒GPU性能增益!正常情况,可以分别在NVIDIA Nsight和微软的GPUView中看到下图所示:

15.2.3.2 Timewarp(TW)

时间扭曲的想法在VR研究中已经存在了几十年,但John Carmack于2014年4月将该特定功能添加到Oculus软件中。Carmack在2013年初首次写下了这个想法,甚至在Oculus DK1发货之前。标准时间扭曲本身并没有实际帮助提高帧速率,也不是有意的,是为了降低VR的感知延迟。Oculus DK1之前的VR的延迟比今天高得多,主要是由于软件而非硬件。Timewarp是Oculus使用的多种软件技术之一,用于将延迟降低到不明显的程度。

Timewarp会在将已渲染帧发送到HMD之前重新投影该帧,以表达头部旋转的变化。也就是说,在帧开始渲染和完成渲染之间,它会沿旋转头部的方向以几何方式扭曲图像。由于这只需要重新渲染所需时间的一小部分,并且帧会立即发送到HMD,因此感知延迟较低,因为结果更接近用户应该看到的内容。

如今,所有主要VR平台都使用时间扭曲的概念。所以,与通常的看法相反,即使达到了全帧速率,仍然会看到被重投影的帧。

假设VR的目标帧率是90fps,因此大约10毫秒,任何GPU停顿都会扼杀体验,如果不能达到目标帧率,时间扭曲就会发生,fps会减半。上下文优先级通过GPU抢占使VR平台供应商能够实现异步时间扭曲。上下文优先级是NVIDIA提供的一个低级别功能,它使VR平台供应商能够实现异步时间扭曲。这样做的方式是通过使用高优先级图形上下文启用GPU抢占。

Timewarp是Oculus SDK中实现的一项功能,它允许我们渲染图像,然后对渲染图像执行后处理,以根据渲染期间头部运动的变化对其进行调整。假设我已经渲染了一个图像,就像你在这里看到的,使用一些头部姿势。但当完成渲染时,玩家的头已经移动了,现在看起来方向略有不同。时间扭曲是一种移动图像的方法,作为一种纯粹的图像空间操作,以补偿这一点。如果我的头向右,它会将图像向左移动,依此类推。由于图像空间扭曲,时间扭曲有时会产生较小失真,但它在减少感知延迟方面却非常有效。

下图是有无时间扭曲的对比图:

启用时间扭曲后(下),我们可以在vsync之前几毫秒重新采样头部姿势,并将刚刚渲染完成的图像扭曲为新的头部姿势,使得我们能够在很大程度上减少感知延迟。

如果使用timewarp,并且游戏以稳定的帧速率渲染,那么这就是几个帧上的计时效果。下图绿色条表示主要的游戏渲染,当玩家四处移动时,需要花费不同的时间,不同的对象会显示在屏幕上。在对每一帧进行游戏渲染之后,会等到vsync之前,再启动时间扭曲(由小青色条表示)。只要游戏在vsync时间限制内持续运行,就非常有效。

15.2.3.3 Async Timewarp(ATW)

在VR渲染中,内容的复杂性各不相同。因此,复杂内容的渲染可能无法在一帧的刷新周期内完成。因此,屏幕刷新后不会生成新内容,用户会将其视为冻结。为了解决这个问题,业界提出了ATW渲染技术。该技术通过姿势预测确定帧中的头部姿势,在生成前一帧图像时根据姿势计算姿势差,根据姿势差更改前一帧图像的位置,并在新帧中生成中间图像,解决了由于缺少当前帧而导致的冻结问题。ATW渲染技术在大多数情况下可以保证用户流畅的视觉体验。理论上,ATW可以基于一帧图像连续生成新图像。然而,由于连续的失真,生成的图像和实际渲染的图像之间的误差将累积。结果,图像质量将恶化。

然而,在vsync上,游戏从来没有100%稳定运行。PC操作系统根本无法保证这一点。每隔一段时间,Windows就会决定开始在后台或其他地方为文件编制索引,而你的游戏就会被耽搁,产生一个卡顿(hitch)。卡顿总是令人讨厌,但在虚拟现实中,它们真的很糟糕。将前一帧卡在头显设备上会导致瞬间眩晕。

这就是异步时间扭曲(Async Timewarp,ATW)的用武之地,想法是让timewarp不必等待应用程序完成渲染。Timewarp的行为应该像GPU上运行的一个单独的进程,它会在vsync、每个vsync之前唤醒并完成它的工作,无论应用程序是否完成渲染。如果我们可以做到这一点,那么只要主渲染过程落后,我们就可以重新扭曲前一帧。因此,我们不必忍受HMD上的图像卡顿;即使应用程序挂接或丢弃帧,我们也将继续进行低延迟头部跟踪。

g)

NVIDIA支持高优先级图形上下文,抢占其他GPU工作,主渲染——正常上下文,时间扭曲渲染——优先级上下文。当前GPU支持绘图级别抢占(preemption),只能在绘制调用边界处切换!长绘制会延迟上下文切换。仍尝试以原生帧速率(90 Hz)渲染!更好的体验是异步时间扭曲是一个安全网,长的绘制可能导致卡顿,拆分时间大于1ms左右的图形,繁重的后处理在屏幕空间分割。

NV还支持直接模式,防止桌面扩展到VR头显,从操作系统隐藏显示,但让VR应用程序直接渲染到其中,以获得更好的用户体验。对于前置缓冲区渲染,D3D11中通常无法访问,但直接模式允许访问前缓冲区,启用低级别延迟优化,vblank期间渲染,存在光束竞争(beam racing)。

异步时间扭曲采用了相同的几何扭曲概念,并使用它来补偿丢失的帧。如果当前帧未及时完成渲染,ATW将使用最新的跟踪数据重新投影前一帧。它被称为“异步”,因为它与渲染并行发生,而不是在渲染之后发生。在知道真实帧是否会按时完成渲染之前,合成帧已准备就绪。

ATW于2014年末首次在Gear VR Innovator Edition上发布。然而,直到2016年3月Rift consumer发布,它才在PC上可用。该功能对最近GPU中添加的硬件功能的依赖是Rift不支持GeForce 7系列卡或R9系列之前的AMD卡的原因之一。2016年10月,Valve向SteamVR添加了一个类似的功能,他们称之为异步重投影。该功能最初仅支持NVIDIA GPU,但在2017年4月增加了对AMD GPU的支持。下面三图阐述了不同模式的游戏循环对比图:

上:基础游戏循环。

中:当帧速率保持不变时,这种体验感觉真实且令人愉快,当它没有及时发生时,会显示前一帧,可能会使人眩晕,中图显示了基本游戏循环中的抖动示例。

下:ATW是一种稍微移动渲染图像以调整头部运动变化的技术。虽然图像已修改,但头部移动不多,因此变化很小。此外,为了解决用户计算机、游戏设计或操作系统的问题,ATW可以帮助修复不规则或帧速率意外下降的时刻。下图显示了应用ATW时帧下降的示例。

15.2.3.4 Interleaved Reprojection(IR)

在将异步重投影添加到SteamVR之前,Valve的平台具有交错重投影(Interleaved Reprojection,IR)。与ATW一样,IR不是一个始终开启的系统,而是由合成器自动打开和关闭。当一个应用程序在几秒钟内持续丢弃多个帧时,IR强制应用程序以半帧速率(45FPS)运行,然后每秒钟合成一帧,因此“交错”。交错重投影实际上比异步重投影有一些感知上的优势,因为它使任何双图像伪影在空间上保持一致。随着2018年SteamVR运动平滑技术的发布,交错重投影技术已经过时。

15.2.3.5 Asynchronous Spacewarp(ASW) / Motion Smoothing

时间扭曲(当前)和重投影仅用于旋转跟踪。它们不考虑头部的位置移动,也不考虑场景中其他对象的移动。2016年12月,Oculus发布了Asynchronous Spacewarp(ASW)来解决这个问题。ASW本质上是一种快速外推算法,它使用前一帧之间的差异(即运动)来估计下一帧应该是什么样子。尽管有名称,ASW并不总是启用的。就像SteamVR过去的交错重投影一样,当一个应用程序在几秒钟内持续丢弃多个帧时,ASW会自动启用。然后,它强制应用程序以半帧速率(45FPS)运行,并每秒合成生成一帧。因此,ASW不能取代ATW,ATW始终处于活动状态,ASW在需要时启动。

由于ASW仅具有帧的颜色信息,而不了解对象的深度,因此图像中通常存在明显的瑕疵。2018年11月,Valve为SteamVR添加了一个类似的功能,他们称之为运动平滑。

Asynchronous Spacewarp 2.0是ASW即将进行的更新,通过结合对深度的理解,大大提高了该技术的质量。在宣布该技术时,Oculus展示了以下场景,作为2.0更新将消除的视觉瑕疵的示例:

然而,与迄今为止所有其他技术不同的是,ASW 2.0不能仅在任何应用程序上运行。开发人员必须在每一帧提交深度缓冲区,否则将退回到ASW 1.0。谢天谢地,虽然Unity和Unreal Engine共同驱动了绝大多数VR应用程序,但现在在使用Oculus集成时默认提交深度。

15.2.3.6 Positional Timewarp (PTW)

PTW是即将对Asynchronous Timewarp(ATW)进行的更新,ATW将使用ASW 2.0用于添加高质量位置校正的相同深度缓冲区。像今天的ATW一样,PTW更新仍将始终处于启用状态,因此一旦帧丢失,合成帧就会及时准备就绪。Facebook声称,PTW使ASW启用或禁用的过渡更加无缝,因为事先不再存在位置抖动。但就像ASW 2.0一样,PTW只适用于提交深度缓冲区的应用程序。据称,PTW将与ASW 2.0进行相同的更新,因为ASW 2.0将不再考虑HMD的移动,这完全取决于PTW。

简而言之,以下是每种技术的作用:

  • 时间扭曲:降低感知延迟。
  • 异步时间扭曲/重投影(ATW):旋转补偿丢失的帧。
  • 异步Spacewarp(ASW)/运动平滑:当帧速率较低时,将应用速度降低到45FPS,并通过外推过去帧的运动,每2帧合成一次。

下面是每种技术的比较:

“自动切换”技术并不总是启用。相反,当合成器注意到帧速率已低达数秒以上时,将启用其中一种模式。启用后,合成器会强制正在运行的应用程序以半帧速率(当前HMD为45 FPS)进行渲染。合成器在分析之前的帧并结合HMD跟踪数据的基础上,合成其他帧。当GPU利用率再次降低时,合成器将禁用该模式并将应用程序返回到90 FPS。

15.2.3.7 优化延迟和滞后

虽然开发人员无法控制系统延迟的许多方面(例如显示更新率和硬件延迟),但确保VR体验不会延迟或丢弃帧是很重要的。许多游戏会因处理和渲染到屏幕上的大量或更多复杂元素而变慢。虽然只是传统视频游戏中的一个小麻烦,但对于VR中的用户来说可能会非常不舒服。将延迟定义为用户头部移动和屏幕上显示的更新图像之间的总时间(运动到光子),它包括传感器响应、融合、渲染、图像传输和显示响应的时间。过去关于潜伏期影响的研究结果有些参差不齐。许多专家建议尽量减少延迟,以减少不适,因为头部运动和显示器上相应更新之间的延迟可能会导致感觉冲突和前庭眼反射错误。因此,鼓励尽可能减少延迟。

值得注意的是,一些关于头戴式显示器的研究表明,固定的等待时间会产生大约相同程度的不适,无论是短至48毫秒还是长至300毫秒;然而,驾驶舱和驾驶模拟器中的可变和不可预测延迟平均时间越长,造成的不适感就越大。这表明,人们最终可以习惯一个一致且可预测的滞后,但平均而言,波动、不可预测的滞后时间越长,就越令人不安。

Oculus官方认为强制VR的阈值应为20毫秒或以下的延迟。超过这个范围,用户报告说在环境中感觉不那么沉浸和舒适。当潜伏期超过60毫秒时,一个人的头部运动和虚拟世界的运动之间的分离开始感觉不同步,导致不适和迷失方向。大量的潜伏期被认为是不适的主要原因之一。与舒适度问题无关,延迟可能会中断用户交互和状态。在理想世界中,离0毫秒越近越好。如果延迟不可避免,那么变化越大,就越不舒服,目标应该是尽可能降低和减少可变延迟。

20世纪和21世纪的计算机图形学的模型对比如下图:

人类感知的极限超越了现代VR设备的10万到100万倍:

优化的方法有分区渲染:

后期跟踪更新——在显示调制过程中插入跟踪,以比帧速率更快地更新位置。

对于XR的双目显示,由于交点和显示平面的不同,存在冲突:

NV的GameWorks VR是用于VR设备和游戏开发的SDK,早在2015年的版本就支持了以下特性:

其中VR SLI是双GPU交火渲染:

其中交叉帧SLI和VR SLI的延迟对比如下:

VR SLI实现示意图如下:

对于VR的两个view,渲染代表的改进如下:

// 优化前
for (each view)
    find_objects();
    for (each object)
        update_constants();
        render();

// 优化后
find_objects();
for (each object)
    for (each view)
         update_constants();
    render();

15.2.4.1 多分辨率渲染和注视点渲染

如果我们在外围渲染低分辨率,必须小心锯齿/闪烁,如果我们在外围渲染平滑的图像模糊,用户会体验到“隧道视觉”效果,研究表明,我们应该提高外围低频内容的对比度。跟踪用户的视线,在距离注视点较远的地方以越来越低的分辨率渲染。

下面是不同的注视点渲染画面对比:

头显为了支持宽FOV,通常用镜头透视来实现,但镜头会引入扰动、枕形畸变和色差(不同波长的光折射量不同)等问题:

摄影镜头畸变的回调软件校正:

VR渲染中镜头畸变的软件补偿,步骤1:使用传统图形管线以每只眼睛的全分辨率渲染场景,步骤2:扭曲图像,使场景在物理镜头扭曲后看起来正确(可以使用对R、G、B的单独畸变来近似校正色差)。

基于光栅化的图形基于到平面的透视投影,根据VR渲染的需要,在高FOV下扭曲图像,VR渲染跨越宽视场。潜在解决方案空间:扭曲显示、光线投射以实现统一的角度分辨率,使用分段线性投影平面进行渲染(每个屏幕分幅的平面不同)。

由于VR扭曲,图像的四个边缘在渲染期间被压缩,并且在从应用程序内容渲染的大量像素重新采样后无法显示。事实上,可以减少这些像素的渲染开销。业界的GPU供应商提出了多分辨率着色技术,以减少渲染开销。在这项技术中,图像被划分为网格。中心区域保留原始分辨率,四个边和角的分辨率分别压缩1/2和1/4(可根据需要更改)。在渲染应用程序内容的过程中,GPU会立即绘制图像。

在VR设备上呈现的图像必须扭曲,以抵消镜头的光学效果。在下图中,一切看起来都是弯曲和扭曲的,但当通过镜头观看时,观众会感觉到一幅未扭曲的图像。

问题是GPU无法以原生方式渲染成这样的扭曲视图,这将使三角形光栅化变得更加复杂。当前的VR平台都解决了这个问题,首先渲染正常图像(左),然后进行后处理,将图像重新采样到扭曲的视图(右)。

如果你观察在变形过程中发生的情况,你会发现,虽然图像的中心保持不变,但边缘却被挤压得很厉害。意味着我们对图像的边缘进行了过度着色。我们正在生成大量的像素,这些像素永远不会显示在屏幕上——它们只是在扭曲过程中被丢弃了,这些像素是浪费的工作,会降低性能。

多分辨率着色的想法是将图像分割为多个视口(下图是一个3x3的网格)。我们保持“中心”视口的大小相同,但缩小边缘周围的所有视口。可以更好地近似于我们想要最终生成的扭曲图像,但不会浪费太多像素。而且,由于我们对像素进行着色处理的数量更少,因此渲染速度更快。根据缩小边缘的力度,可以在任何位置保存25%到50%的像素,转化为1.3倍到2倍的像素着色加速。

以下是几种渲染模式的着色像素对比:

15.2.4.2 立体和多视图渲染

视差是投影到两个立体图像中的3D点的相对距离,也是实现立体图像的常用技术,下图是三种不同的视差案例:

视觉系统仅使用水平视差,无垂直视差!粗略的前束法(toe-in)造成垂直视差,引起视觉不适(下图左):

使用OpenGL/WebGL进行立体渲染:视图矩阵。需要修改视图矩阵和投影矩阵,渲染管线不变–仅这两个矩阵,但是需要按顺序渲染两幅图像。首先查看视图矩阵,编写自己的lookAt函数,该函数使用旋转和平移矩阵从eye、center、up参数生成视图矩阵,不要使用THREE.Matrix4().lookAt()函数,它不能正常工作!下面是使用OpenGL构造立体视图矩阵的过程:

上面讨论的透视投影是轴=对称的,我们还需要一种不同的方法来设置非对称离轴平截头体,可以使用THREE.Matrix4().makePerspective(left,right,top,bottom,znear,zfar)

轴上和离轴的视锥体构造示意图如下:

使用OpenGL绘制立体图最有效的方式:

1、清晰的颜色和深度缓冲区。

2、设置左侧模型视图和投影矩阵,仅将场景渲染到红色通道。

3、清除深度缓冲区。

4、设置右侧模型视图和投影矩阵,仅将场景渲染到绿色和蓝色通道中。

我们将以稍微复杂一点的方式完成(无论如何都需要其他任务):多个渲染过程,渲染到屏幕外(帧)缓冲区。

OpenGL帧缓冲区通常(帧)缓冲区由窗口管理器(即浏览器)提供,对于大多数单通道应用程序,有两个(双)缓冲区:后缓冲区和前缓冲区渲染到后缓冲区;完成后交换缓冲区(WebGL为您完成此操作!)。优点是渲染需要时间,不想让用户看到三角形是如何绘制到屏幕上的;仅显示最终图像,在许多立体声应用中,4个缓冲区:前/后左和右缓冲区,将左右图像渲染到后缓冲区中,然后将两者交换在一起。

更通用的方式是使用离屏缓冲区。OpenGL中最常见的屏幕外缓冲区形式:帧缓冲区对象,采用“渲染到纹理”的概念,但具有颜色、深度和其他重要的每片段信息的多个“附件”,尽可能多的帧缓冲区对象,它们都“活动”在GPU上(无内存传输),每种颜色的位深度:8位、16位、32位用于颜色附件;深度为24位。

FBO对于多个渲染通道至关重要!第1个通道:渲染FBO的颜色和深度,第2个通道:渲染纹理矩形–访问片段着色器中的FBO。

为了模拟人眼视网膜的模糊(失焦)效果,需要DOF(景深)的后处理来达成。方法有很多种,此处忽略。

与常见的应用程序渲染不同,每个帧的VR渲染需要同时渲染左眼和右眼的图像。在每个帧中,分别为左眼和右眼上的图像提交一个渲染任务。因此,VR渲染所占用的CPU/GPU资源是普通应用程序渲染所占用资源的两倍。为了解决这个问题,业界提出了多视图渲染技术,这样在只提交一个任务后,就可以同时渲染左眼和右眼的图像。左眼和右眼的图像的大部分信息是相同的,并且图像的视差仅略有不同。因此,在多视图渲染技术中,CPU只需向GPU提交一个渲染任务和视差信息,然后GPU就可以为左眼和右眼渲染图像,大大减少了CPU资源占用,提高了帧速率。

用于优化VR等渲染的MultiView对比图。上:未采用MultiView模式的渲染,两个眼睛各自提交绘制指令;中:基础MultiView模式,复用提交指令,在GPU层复制多一份Command List;下:高级MultiView模式,可以复用DC、Command List、几何信息。

Unity支持以下几种VR渲染模式:

  • Multi-Camera。为了为每只眼睛渲染视图,最简单的方法是运行渲染循环两次。每只眼睛将配置并运行自己的渲染循环迭代。最后,将有两个图像可以提交到显示设备。底层实现使用两个Unity摄像头,每只眼睛一个,它们贯穿生成立体图像的过程。这是Unity中支持XR的最初方法,目前仍由第三方HMD插件提供。虽然这种方法确实有效,但多摄像头依赖于暴力,就CPU和GPU而言,效率最低。CPU必须在渲染循环中完全迭代两次,GPU很可能无法利用对眼睛绘制两次的对象的任何缓存。

  • Multi-Pass。Multi-Pass是Unity优化XR渲染循环的最初尝试,核心思想是提取与视图无关的渲染循环部分,意味着任何不明确依赖于XR eye viewpoints的工作都不需要针对每只眼睛进行。这种优化最明显的候选者是阴影渲染,阴影并不明确依赖于摄影机查看器的位置。Unity实际上分两步实现阴影:生成级联阴影贴图,然后将阴影映射到屏幕空间。对于多通道,可以生成一组级联阴影贴图,然后生成两个屏幕空间阴影贴图,因为屏幕空间阴影贴图取决于查看器的位置。由于阴影生成的架构,屏幕空间阴影贴图受益于局部性,因为阴影贴图生成循环相对紧密耦合。可以与剩余的渲染工作负载进行比较,剩余的渲染工作负载需要在返回到类似阶段之前在渲染循环上进行完全迭代(例如,眼睛特定的不透明过程由剩余的渲染循环阶段分隔)。另一个可以在两只眼睛之间共享的步骤一开始可能并不明显:可以在两只眼睛之间执行一次剔除。在最初的实现中,使用了截锥剔除来生成两个对象列表,每只眼睛一个。然而,可以创建一个两只眼睛共享的统一剔除截锥。意味着每只眼睛的渲染量都会比使用单眼剔除截头体时稍微多一些,但单次剔除的好处超过了一些额外顶点着色器、剪裁和光栅化的成本。

  • Single-Pass。单通道立体渲染意味着将对整个renderloop进行一次遍历,而不是两次或某些部分两次。

    为了执行这两个绘制,需要确保所有常量数据都已绑定,并且还有一个索引。绘制结果如何?如何进行每次绘制调用?在多通道中,两只眼睛都有自己的渲染目标,但不能在单通道中这样做,因为在连续绘制调用中切换渲染目标的成本太高。一个类似的选项是使用渲染目标数组,但需要在大多数平台上从几何体着色器导出切片索引,这种操作在GPU上也可能很昂贵,并且对现有着色器具有侵入性。

    确定的解决方案是使用双宽(Double Wide)渲染目标,并在绘制调用之间切换视口,允许每只眼睛渲染到双宽渲染目标的一半。虽然切换视口确实会带来成本,但它比切换渲染目标要少,并且比使用几何体着色器(尽管Double Wide有其自身的一系列挑战,尤其是在后处理方面)。还有使用视口数组的相关选项,但它们与渲染目标数组有相同的问题,因为索引只能从几何体着色器导出。

    现在有了一个解决方案,可以开始两次连续绘制以渲染双眼,需要配置支持基础设施。在多通道中,因为它类似于单视图渲染,所以可以使用现有的视图和投影矩阵基础结构,只需将视图和投影矩阵替换为来自当前眼睛的矩阵。然而,对于单通道,不想不必要地切换常量缓冲区绑定。因此,可以将双眼的视图和投影矩阵绑定在一起,并使用unity_StereoEyeIndex对其进行索引,可以在绘图之间进行翻转。这允许着色器基础结构在着色器过程中选择要渲染的视图和投影矩阵集。

    另一个细节:为了最小化视口和unity_StereoEyeIndex状态的更改,可以修改眼睛绘制模式,可以使用left、right、right、left、right等节奏,而不是绘制left、right、left、left等。这使我们能够将状态更新的数量减少一半,而不是交替的节奏。比多通道快不了两倍,因为已经针对消隐和阴影进行了优化,同时仍在调度每只眼睛绘制和切换视口,确实会产生一些CPU和GPU成本。

  • Stereo Instancing (Single-Pass Instanced)。渲染目标数组是立体渲染的自然解决方案。眼睛纹理共享格式和大小,使其符合在渲染目标阵列中使用的条件,但使用几何体着色器导出数组切片是一个很大的缺点。我们真正想要的是能够从顶点着色器导出渲染目标数组索引,从而实现更简单的集成和更好的性能。从顶点着色器导出渲染目标数组索引的功能实际上存在于某些GPU和API上,并且越来越普遍。在DX11上,此功能作为功能选项VPAndRTArrayIndexFromAnyShaderFeedingRasterizer公开。

    现在我们可以指定渲染目标数组的哪个切片,如何选择该切片?我们利用单通道双宽的现有基础架构。我们可以使用unity_StereoEyeIndex在着色器中填充SV_RenderTargetArrayIndex语义。在API方面,我们不再需要切换视口,因为相同的视口可以用于渲染目标数组的两个切片。我们已经将矩阵配置为从顶点着色器索引。

    虽然我们可以继续使用现有的技术,即在每次绘制之前发出两次绘制并在常量缓冲区中切换值unity_StereoEyeIndex,但还有一种更有效的技术。我们可以使用GPU实例化来发出单个绘图调用,并允许GPU跨双眼多路复用绘制。我们可以将绘制的现有实例数加倍(如果没有实例使用,我们只需将实例数设置为2)。然后在顶点着色器中,我们可以对实例ID进行解码,以确定渲染到哪只眼睛。

    使用此技术的最大影响是,我们实际上将在API端生成的绘制调用数量减少了一半,从而节省了大量CPU时间。此外,GPU本身能够更高效地处理绘图,即使生成的工作量相同,因为它不必处理两个单独的绘制调用。我们还可以通过不必在绘制之间更改视口来最小化状态更新,就像我们在传统的单过程中所做的那样。

    请注意:此方法仅适用于在Windows 10或HoloLens上运行桌面VR体验的用户。

  • Single-Pass Multi-View。Multi-View是某些OpenGL/OpenGL ES实现中可用的扩展,其中驱动程序本身处理双眼之间的单个绘制调用的多路复用。驱动程序负责复制绘制并在着色器中生成数组索引(通过gl_ViewID),而不是显式实例化绘制调用并将实例解码为着色器中的眼睛索引。有一个与立体实例化不同的底层实现细节:驱动程序本身决定渲染目标,而不是顶点着色器显式选择将被光栅化到的渲染目标数组切片。gl_ViewID用于计算视图相关状态,但不用于选择渲染目标。在使用中,这对开发人员来说并不重要,但却是一个有趣的细节。由于我们如何使用多视图扩展,我们能够使用为单过程实例构建的相同基础结构,开发人员可以使用相同的功能(scaffolding)来支持这两种单通道技术。

以下是Unity不同的VR渲染技术的性能对比:

正如上图所示,单通道和单通道实例化代表了与多过程相比的显著CPU优势。但是,单通道和单通道实例化之间的增量相对较小,原因是切换到单通道已经节省了大量CPU开销。单通道实例化确实减少了绘制调用的数量,但与处理场景图相比,这一成本非常低。当考虑到大多数现代图形驱动程序都是多线程的时候,在调度CPU线程上发出draw调用可能会非常快。

15.2.4.3 光场渲染

现有的VR成像方法基本上是具有双目视差的2D成像方法。眼睛的焦点和汇聚点不会长时间保持在同一位置。前者位于屏幕平面上,后者位于双目视差生成的虚拟平面上。因此,会发生边缘调节冲突,导致头晕等生理不适和沉浸感丧失。

恢复现实世界中肉眼可见的内容可以恢复完美的沉浸感。通过改变焦距,眼睛可以在不同距离、不同位置和不同方向上收集物体表面反射的光。这一切的完整集合就是光场。该行业的一家供应商开发了一种光场摄像机,用于收集光场信息。光场渲染技术恢复采集到的光场信息,以满足用户更高的沉浸体验要求。光场信息的采集、存储和传输仍然面临着大量数据等许多基本问题,光场渲染技术仍处于初级阶段。然而,随着用户对VR体验的要求越来越高,它可能成为未来关键的渲染技术。

光场显示图示。

光场显示头显。

注视点光场渲染。

15.2.4.4 光影

对于切线空间轴对齐的各向异性照明,标准各向同性照明沿对角线表示,各向异性与任一相切空间轴对齐,只需要2个附加值与2D切线法线配对=适合RGBA纹理(DXT5>95%的时间)。

粗糙度到指数的转换:漫反射照明将Lambert提高到指数(\(N\cdot L^k\)),其中\(k\)在0.6-1.4范围内尝,试了各向异性漫反射照明,但不值得这么做,镜面反射指数范围为1-16384,是具有各向异性的修改的Blinn-Phong。

void RoughnessEllipseToScaleAndExp(float2 vRoughness, out float o_flDiffuseExponentOut,out float2 o_vSpecularExponentOut,out float2 o_vSpecularScaleOut)
{
    o_flDiffuseExponentOut=((1.0-(vRoughness.x+ vRoughness.y) * 0.5) *0.8)+0.6;// Outputs 0.6-1.4
    o_vSpecularExponentOut.xy=exp2(pow(1.0-vRoughness.xy,1.5)*14.0);// Outputs 1-16384
    o_vSpecularScaleOut.xy=1.0-saturate(vRoughness.xy*0.5);//This is a pseudo energy conserving scalar for the roughness exponent
}

各向异性的光照计算过程:

几何镜面锯齿:没有法线贴图的密集网格也会产生锯齿,粗糙度mips也无济于事!可以使用插值顶点法线的偏导数来生成近似曲率的几何粗糙度项。

float3 vNormalWsDdx = ddx(vGeometricNormalWs.xyz);
float3 vNormalWsDdy = ddy(vGeometricNormalWs.xyz);
float flGeometricRoughnessFactor = pow(saturate(max(dot(vNormalWsDdx.xyz, vNormalWsDdx.xyz), dot(vNormalWsDdy.xyz, vNormalWsDdy.xyz))), 0.333);
vRoughness.xy=max(vRoughness.xy, flGeometricRoughnessFactor.xx); // Ensure we don’t double-count roughness if normal map encodes geometric roughness

flGeometricRoughnessFactor的可视化。

MSAA中心与质心插值并不完美,因为过度插值顶点法线,法线插值可能会在轮廓处导致镜面反射闪烁。下面是文中使用的一个技巧:

// 插值法线两次:一次带质心,一次不带质心
float3 vNormalWs:TEXCOORD0;
centroid float3 vCentroidNormalWs:TEXCOORD1;

// 在像素着色器中,如果法线长度平方大于1.01,请选择质心法线
if(dot(i.vNormalWs.xyz, i.vNormalWs.xyz) >= 1.01)
{
    i.vNormalWs.xyz = i.vCentroidNormalWs.xyz;
}

法线贴图编码:将切线法线投影到Z平面上仅使用2D纹理范围的约78.5%,而半八面体编码使用2D纹理的全部范围:

事实证明,1.4x只是HTC Vive的一个建议(每个HMD设计都有一个基于光学和面板的不同建议标量),在较慢的GPU上,缩小建议的渲染目标标量,在速度更快的GPU上,放大建议的渲染目标标量,尽量利用GPU的周期。

提高了显示器的分辨率(别忘了,VR的每度只有更少的像素),对于颜色和法线贴图,强制启用此选项,默认使用8x。禁用其它所有功能,仅三线性,但需要测量性能。如果在其它地方遇到瓶颈,各向异性过滤可能是“免费的”。

噪点是良师益友,在虚拟现实中,过渡很可怕,带状(banding)比液晶电视更明显,当像素着色器中有浮点精度时,可在帧缓冲区中添加噪点。

float3 ScreenSpaceDither(float2vScreenPos)
{
    // Iestyn's RGB dither(7 asm instructions) from Portal 2X360, slightly modified for VR
    float3 vDither = dot(float2(171.0, 231.0), vScreenPos.xy + g_flTime).xxx;
    vDither.rgb = frac(vDither.rgb / float3(103.0, 71.0, 97.0)) - float3(0.5, 0.5, 0.5);
    return (vDither.rgb / 255.0) * 0.375;
}

对于环境图,无穷远处的标准实现 = 仅适用于天空,需要为环境图使用某种类型的距离重新映射:球体很便宜,立方体更贵,两者在不同的情况下都很有用。

需要性能查询!总是保持垂直同步,禁用VSync查看帧率会让玩家头晕,需要使用性能查询来报告GPU工作负载,最简单的实现是测量从第一个到最后一个draw调用。理想情况下,测量以下各项:从Present()到第一次绘图调用的空闲时间、从第一次绘图调用到最后一次绘图调用、从上次绘图调用到现在的Present()的空闲时间。

15.2.4.5 射线检测

光线投射是VR/AR光栅化的可行替代方法。在VR中,一组新的要求是广阔的视野、透镜畸变、亚像素渲染、低延迟、滚动显示校正、景深、高分辨率和帧速率、注视点渲染、高效的抗锯齿等。

上述特性在不同的渲染方式支持表如下:

虚拟现实的层次可见性:每秒海量射线,包括商品硬件上的着色,完全动态场景支持,任意相干射线分布,包括非点原点!光线采样层次的构建过程示意图如下:

层次采样可获得缓存命中率的提升,亦即获得性能的提升:

15.2.4.6 抗锯齿

常见的抗锯齿方法有:

  • 边缘几何AA,通常硬件加速;
  • 图像空间AA,非常适合大多数渲染管线,如FXAA、MLAA、SMAA等;
  • 时间AA,使用再投影进行时间超采样。

MSAA在高频几何体、几乎垂直的线条、对角线看起来更好,但内部纹理/着色仍然有锯齿。

FXAA在边缘几何体、纹理/着色细节看起来更好,但有时会丢失高频数据中的细节。

超采样反走样渲染到更大缓冲区的效果良好……如果负担得起的话,与良好的下采样过滤器一起使用。

镜面AA也可以大大改善图像,一个很好的起点是研究LEAN、Cheap LEAN (CLEAN)和Toksvig AA,扭曲着色器减少边缘锯齿,在某些游戏中,可能需要更多地关注LOD。几种AA方法的组合可能会产生更好的结果,每种不同的AA解决方案都能解决不同方面的锯齿问题,使用最适合引擎的方法。

锯齿是VR的头号敌人:相机(玩家的头)永远不会停止移动,因此,锯齿会被放大。虽然要渲染的像素更多,但每个像素填充的角度比以前做的任何事情都大,以下是一些平均值:2560x1600 30英寸显示器:约50像素/度(50度水平视场),720p 30英寸显示器:约25像素/度(50度水平视场),VR:约15.3像素/度(110度视场,是非VR的1.4倍),必须提高像素的质量。

4xMSAA最低质量:前向渲染器因抗锯齿而获胜,因为MSAA正好有效,如果性能允许,使用8xMSAA,必须将图像空间抗锯齿算法与4xMSAA和8xMSAA并排进行比较,以了解渲染器将如何与业内其它渲染器进行比较,使用HLSL的“sample”修饰符时,抖动的SSAA显然是最好的,但前提是可以节省性能。

法线贴图依然可用,大多数法线贴图在虚拟现实中效果都很好。无效的情况:跟踪体积内大于几厘米的特性细节不好,以及被跟踪体积内的表面形状不能在法线贴图中。有效的情况:无法近距离查看的被跟踪体积外的远处物体,以及表面“纹理”和精细细节。法线贴图映射错误:

任何只生成平均法线的mip过滤器都会丢失重要的粗糙度信息:

用Mips编码的粗糙度:可以存储一个各向同性值(可视为圆的半径),是所有2D切线法线与促成该纹理的最高mip的标准偏差,还可以分别存储X和Y方向标准偏差的二维各向异性值(可视化为椭圆的尺寸),该值可用于计算切线空间轴对齐的各向异性照明

添加艺术家创作的粗糙度,创作了2D光泽=1.0–粗糙度,带有简单盒过滤器的Mip,将其与每个mip级别的法线贴图粗糙度相加/求和,因为有各向异性光泽贴图,所以存储生成的法线贴图粗糙度是免费的。

左:各向同性光泽度;右:各向异性光泽度。

单GPU情况下,单个GPU完成所有工作,立体渲染可以通过多种方式完成(本例使用顺序渲染),阴影缓冲区由两只眼睛共享。多GPU亲和API,AMD和NVIDIA有多个GPU亲和API,使用关联掩码跨GPU广播绘制调用,为每个GPU设置不同的着色器常量缓冲区,跨GPU传输渲染目标的子矩形,使用传输栅栏在目标GPU仍在渲染时异步传输。2个GPU时,每个GPU渲染一只眼睛,两个GPU都渲染阴影缓冲区,“向左提交”和“应用程序窗口”在传输气泡中执行,性能提高30-35%。4个GPU时,每个GPU渲染一只眼睛的一半,所有GPU渲染阴影缓冲区,PS成本成线性比例,VS成本则不是,驱动程序的CPU成本可能很高。

从上到下:1个、2个、4个GPU渲染示意图。

投影矩阵与VR光学:投影矩阵的像素密度分布与我们想要的相反,投影矩阵在边缘每度像素密度增加,VR光学在中心像素密度增加,我们最终在边缘过度渲染像素。使用NVIDIA的“多分辨率着色”,可以在更少的CPU开销下获得额外约5-10%的GPU性能。

径向密度遮蔽(Radial Density Masking):跳过渲染2x2像素方块的棋盘格图案,以匹配当前的GPU架构。

重建滤波器的过程如下:

径向密度遮蔽的步骤:

  • 渲染时Clip掉2x2的像素方块,或使用2x2的棋盘格图案填充模板或深度,然后进行渲染。
  • 重建滤波器。

在Aperture Robot Repair中节省5-15%的性能,使用不同的内容和不同的着色器可以获得更高的增益,如果重建和跳过像素的开销没有超过跳过像素四元体的像素着色器节省,那么就是一次wash。在低端GPU上几乎总是能节省很多工作。

处理漏帧,如果引擎未达到帧速率,VR系统可以重用最后一帧的渲染图像并重新投影:仅旋转重投影、位置和旋转重投影,用重投影来填充缺失的帧应该被视为最后的安全网。请不要依赖重投影来维持帧率,除非目标用户使用的GPU低于应用程序的最低规格。

仅旋转重投影:抖动是由摄影机平移、动画和被跟踪控制器移动的对象引起的,抖动表现为两个不同的图像平均在一起。

旋转重投影是以眼睛为中心,而不是以头部为中心,所以从错误的位置重投影,ICD(摄像机间距离)根据旋转量在重投影过程中人为缩小。

好的一面:几十年来,人们对算法有了很好的理解,并且可能会随着现代研究而改进,即使有已知的副作用,它也能很好地处理单个漏帧。所以…有一个非常重要的折衷方案,它已足够好,可以作为错过帧的最后安全网,总比丢帧好。

位置重投影:仍然是一个非常感兴趣的尚未解决的问题,在传统渲染器中只能获得一个深度,因此表示半透明是一个挑战(粒子系统),深度可能存储在已解析颜色的MSAA深度缓冲区中,可能会导致颜色溢出。对于未表示的像素,孔洞填充算法可能会导致视网膜竞争,即使有许多帧的有效立体画面对,如果用户通过蹲下或站起来垂直移动,也有需要填补空白。

异步重投影:理想的安全网,要求抢占粒度等于或优于当前一代GPU,根据GPU的不同,当前GPU通常可以在draw调用边界处抢占

,目前还不能保证vsync能够及时重新发布。应用程序需要了解抢占粒度。

交错重投射提示:旧的GPU不能支持异步重投影,所以需要一个替代方案,OpenVR API有一个交错重投影提示,如果底层系统不支持始终开启的异步重投影,应用程序可以每隔一次请求仅限帧旋转的重投影。应用程序获得约18毫秒/帧的渲染。当应用程序低于目标帧率时,底层VR系统还可以使用交错重投影作为自动启用的安全网,每隔一帧重新投影是一个很好的折衷。

维持帧率很难,虚拟现实比传统游戏更具挑战性,因为用户可以很好地控制摄像机,许多交互模型允许用户重新配置世界,可以放弃将渲染和内容调整到90fps,因为用户可以轻松地重新配置内容,通过调整最差的20%体验,让Aperture Robot Repair达到了帧率。

自适应质量:它通过动态更改渲染设置以保持帧率,同时最大限度地提高GPU利用率。目标是减少掉帧和重投影的机会和在有空闲的GPU周期时提高质量。例如,Aperture Robot Repair VR演示使用两种不同的方法在NVIDIA 680上以目标帧率运行。利益是适用于应用程序的最低GPU规格,增加了艺术资产限制——艺术家现在可以在保真度稍低的渲染与更高的多边形资产或更复杂的材质之间进行权衡,不需要依靠重投影来维持帧率,意想不到的好处:应用程序在所有硬件上都看起来更好。

在VR中,无法调整的:无法切换镜面反射等视觉功能,无法切换阴影。可以调整的内容:渲染分辨率/视口(也称为动态分辨率)、MSAA级别或抗锯齿算法、注视点渲染、径向密度遮蔽等。自适应质量示例(黑体是默认配置):

测量GPU工作负载:GPU工作负载并不总是稳定的,可能有气泡,VR系统GPU的工作量是可变的:镜头畸变、色差、伴侣边界、覆盖等。从VR系统而不是应用程序获取计时,例如,OpenVR提供了一个总的GPU计时器用于计算所有GPU工作。

GPU定时器-延迟,GPU查询已经有1帧了,队列中还有1到2个无法修改的帧。

实现细节——3条规则,目标是保持70%-90%的GPU利用率。

  • 高GPU利用率=帧的90%(10.0ms),大幅度降低:如果最后一帧在GPU帧的90%阈值之后完成渲染,则降低2级,等待2帧。
  • 低GPU利用率=帧的70%(7.8毫秒),保守地增加:如果最后3帧完成时低于GPU帧的70%阈值,则增加1级,等待2帧。
  • 预测=帧的85%(9.4ms),使用最后两帧的线性外推来预测快速增长,如果最后一帧高于85%阈值,线性外推的下一帧高于高阈值(90%),则降低2个级别,等待2帧。

10%空闲的规则:90%的高阈值几乎每帧都会让10%的GPU空闲用于其它处理,是件好事。需要与其它处理共享GPU,即使Windows桌面每隔几帧就需要一块GPU。对GPU预算的心理模型从去年的每帧11.11ms变为现在的每帧10.0ms,所以你几乎永远不会饿死GPU周期的其它处理。

解耦CPU和GPU性能,使渲染线程自治,如果CPU没有准备好新的帧,不要重投影!相反,渲染线程使用更新的HMD姿势和动态分辨率的最低自适应质量支持重新提交最后一帧的GPU工作负载。要解决动画抖动问题,请为渲染线程提供两个动画帧,可以在它们之间进行插值以保持动画更新,但是,非普通的动画预测是一个难题。然后,可以计划以1/2或1/3 GPU帧速率运行CPU,以进行更复杂的模拟或在低端CPU上运行。

总之,所有虚拟现实引擎都应支持多GPU(至少2个GPU),注视点渲染和径向密度遮蔽是有助于抵消光学与投影矩阵之争的解决方案,Adaptive Quality可上下缩放保真度,同时将10%的GPU用于其它进程,不要依靠重投影来达到最小规格的帧率!考虑引擎如何通过在渲染线程上重新提交来分离CPU和GPU性能。

技术和设计的优化思路是边开发边优化,编码和构建网格以实现可扩展性,跨所有计划的VR支持硬件进行测试,尽早发现问题。性能方面,可以使用mocap:以动作为导向,支持VR现场表演的表演风格,在VR中指导VR,块状化(blocking)以充分利用空间。

在近两年,移动VR的新挑战是具有长视线的可探索世界,更多角色,更长、更具互动性的电影,拥有各种武器、库存和收藏品等。游戏线程挑战是移动复杂组件层次结构,诞生/销毁卡顿,战斗中的非战斗更新,CPU停顿。

组件层次结构的一般情况:Actor组件而非场景组件,范围内的移动,复杂层次结构每帧最多移动一次,需要时拆离/重新附加。组件层次结构的骨架网格:分离优化:分离骨架网格组件,使用动画图形将根骨骼移动到其应位于的位置,用于玩家棋子、所有敌人和战斗中出现的所有电影角色。缺点是某些动画节点需要修复,部件位置不再正确。组件层次结构的重叠:默认情况下会出现大量不必要的重叠,UE物理/碰撞选项培训,如果可能,切换到保留目标列表。

大多数卡顿问题(hitch)来自actor和组件的诞生/销毁,通过池系统重用对象,提前生成所有内容,使用最高限制。对于非战斗逻辑,添加玩家距离系统以减少战斗中的影响,如果玩家距离太远,则取消放置的物品。游戏线程停顿的原因可能是Quest设备只有少量核心,渲染、音频和游戏同时要求,有些仍然可以解决。Unreal Insights可以帮助查明停顿的原因,对性能的影响比stat capture小,缺点是任务设置很棘手,从4.24开始,没有对象名称使解释变得棘手,无法启动/停止捕获。游戏线程停顿的提示:了解任务图系统,注意勾选先决条件,并行作业可能会被迫提前完成,导致停顿。

其他游戏线程提示:无蓝图tick,tick上没有蓝图可实现/蓝图原生事件,支持非动态委托,谨防蓝图计时器/时间表。

渲染的挑战包含内存、GPU、绘制调用、复杂着色器、复杂的动画、复杂的环境等因素。预计算的可见性(PCV),避免视觉跳变,高采样设置,最大化偶然性,小单元格。效率:设置/维护,计算时间。PCV步骤包含选择性网格放置、导航网格单元放置、世界设置公开配置(栈的单元格数、采样设置、网格数量阈值):

测试场景的各个阶段瓶颈一览:

此外,可以启用HLOD(层次LOD)、自定义HLOD:

始终打开HLOD,消除过渡POP,消除光照贴图LOD POP,删除源网格,增加光照图分辨率,减少PCV计算,保持实例化碰撞。动态分辨率可以动态调整视口大小,不会产生额外的成本,利用Oculus Rift动态分辨率覆盖。

视觉增强功能:菜单的立体图层(口袋)、移动端视差反射、前向渲染贴花。顶点动画:电影级rigid解算器工具,4000多个动画对象,插值,程序性覆盖。顶点变形和实例化:多个实例,变体,带烘焙光探针采样的自发光。顶点动画和实例化:电影管线的群体工具,200个动画角色,通过图集化实现额外变化。

在UI和场景元素中使用易于阅读的文本。有几种方法可以确保VR中的文本易读性。出于渲染目的,建议在应用程序中使用带符号的距离场字体,可以确保字体即使在缩放或缩小时也能平滑呈现。还应该考虑应用程序支持的语言,组合字母组合的复杂性可能会影响易读性,例如,应用程序可能希望使用一种能够很好地支持东亚语言的字体。本地化还可能影响文本布局,因为某些语言在同一副本中使用的字母比其他语言多。场景中的字体大小和位置也很重要,对于Gear VR,选择大于30 pt的字体通常会在4.5m(单位)的固定z深度处提供最小的易读性,大于48磅通常可以确保舒适的阅读体验。对于Rift,大于25 pt的字体大小将在4.5m(统一)的固定z深度处提供最小的易读性,大于42磅通常可以确保舒适的阅读体验。

闪烁在模拟器疾病的动眼神经成分中起着重要作用,通常被视为部分或全部屏幕上亮度和黑暗的快速“脉冲”。用户感知闪烁的程度是多个因素的函数,包括:显示器在“开”和“关”模式之间循环的速率、“开”阶段发出的光量,视网膜的哪些部分受到刺激,甚至是一天中的时间和个人的疲劳程度。虽然闪烁会随着时间的推移变得不那么明显,但它仍然会导致头痛和眼睛疲劳。有些人对闪烁极为敏感,因此会感到眼睛疲劳、疲劳或头痛。其他人甚至不会注意到它或有任何不良症状。尽管如此,仍有某些因素可以增加或减少任何给定人员感知显示闪烁的可能性。

首先,人们对周围的闪烁比视觉中心的闪烁更敏感。其次,屏幕图像越亮,闪烁就越多。明亮的图像,尤其是外围(例如,站在明亮的白色房间中)可能会产生明显的显示闪烁。尽可能使用较深的颜色,尤其是玩家视角中心以外的区域。通常,刷新率越高,闪烁越不易察觉。

不要故意创建闪烁的内容。高对比度、闪光(或快速交替)刺激可引发某些人的光敏性癫痫发作。与此相关的是,高空间频率纹理(如精细的黑白条纹)也可以触发光敏性癫痫发作。国际标准组织发布了ISO 9241-391:2016作为图像内容标准,以降低光敏性癫痫发作的风险,该标准解决了潜在的有害闪光和模式。必须确保内容符合图像安全方面的标准和最佳做法。

使用视差贴图代替法线贴图。法线贴图提供真实的照明提示,以传达深度和纹理,而无需添加给定3D模型的顶点细节。虽然在现代游戏中广泛使用,但在立体3D中观看时,它的吸引力要小得多。因为法线贴图不考虑双目视差或运动视差,所以它会生成类似于绘制在对象模型上的平面纹理的图像。视差映射建立在法线映射的基础上,但法线映射不能解释景深。视差贴图通过使用内容创建者提供的附加高度贴图来移动采样表面纹理的纹理坐标。使用在着色器级别计算的逐像素或逐顶点视图方向应用纹理坐标偏移。视差贴图最好用于具有不会影响碰撞曲面的精细细节的曲面,例如砖墙或鹅卵石路径。

为开发的平台应用适当的失真校正。VR设备中的镜头会扭曲渲染图像,此失真通过SDK中的后处理步骤进行校正。根据SDK指南正确执行此失真非常重要,不正确的失真可以“看起来”相当正确,但仍然会感到迷失方向和不舒服,因此关注细节至关重要。所有扭曲校正值都需要与物理设备匹配,其中任何一个都不能由用户调整。

总之,详细的优化方法,解锁视觉改善技术,增强的交互和游戏机制,Quest 2有着显著的性能提升和用户体验提升。

更多移动端XR优化可参见:剖析虚幻渲染体系(12)- 移动端专题Part 3(渲染优化)和章节12.6.5 XR优化

早期的XR SDK通常可以有限地支持SLAM定位和深度映射,并且同时地定位和映射:创建地图,同时跟踪您在其中的位置,最初是为机器人技术开发的,包括第一艘火星探测器,新设备具有额外的处理能力,包括所谓的“MVU”来帮助处理。SLAM限制是凌乱的房间比干净的房间好,运动模糊、照明会导致与图像目标识别和跟踪类似的问题。深度摄影机限制是分辨率低于彩色摄像机,需要插值深度点,低帧速率,网格化时必须非常缓慢地移动相机,IR不适用于反射表面(窗户、镜子等)。

对于VR的音频,虽然电影声音和游戏声音之间可能存在相似之处,但在将声音理论和概念从两者转换为电影VR时,也存在着值得注意的显著差异。

沉浸(Immersion)和现场(Presence)是两件不同的事情,身临其境的音频是实现存在感的一个关键因素,以及电影摄影、阻挡、表演等,而不仅仅是使用位置音频。VR中的FOA有4种声音:一种是被电影人物感知和理解,并在当前视野中可视化(下图上);还有一种是电影角色感知和理解的声音,但不在当前的fov范围内(下图下);还有非/画外音——角色听不到,但观众认为是伴随着屏幕上的动作,可能是解释动作;以及METADIEGETIC声音——观众角色的想象或幻觉,它是VE的一部分,但其他角色听不到。

Conemarching in VR: Developing a Fractal experience at 90 FPS阐述了VR下的分形算法和RayMarch的优化技术。

左:射线行进优化,其思想是以不同的分辨率逐步渲染同一场景,每次通过时,球体跟踪,直到我们不能保证没有交点为止,将分辨率提高一倍并重复使用距离,通常需要一些偏差。右:为了解决渲染所有内容两次,可以重新投射深度,使用圆锥体绘制器渲染中心眼,重新投射到左眼和右眼,屏幕空间光线水平偏移行进,为了获得更好的聚集距离,在较低分辨率下使用锥形通道。

使用conemarcher,必须计算两次深度,现在可以直接切断大部分管线,重投影通道通常很快,性能与控制过程成比例提高,着色过程需要进行一些调整,因为某些重投影估计值并不完美。

渲染着色仍然很昂贵,不需要太多关于外围的细节,需要一些动态的东西,可以根据分形进行缩放,并且取决于硬件,在外围以半分辨率渲染,在中心以全分辨率渲染,并混合边(图左和图中)。最后合成结果,较强的暗角节省了一些计算时间(图右)。

此外,在VR中,阴影、法线、遮挡、SSS开销都很大!文中提出了不少优化措施,包含:不要渲染太远,使用均匀散射将其隐藏,深度估计的时间重投影,低频效应重投影,使用屏幕空间法线减少着色复杂性,在外围使用较低质量的着色,使用立体重投影视差提供的轮廓实现TAA+FXAA。

CocoVR - Spherical Multiprojection介绍了球形多投影技术,包含球面投影简介、实践中的球面投影、Art管线、算法细节、开发工具等。球形投影类似于拍摄360度图像并将其应用于skydome,将其应用于场景中的大部分几何体,而不是应用于背景以模拟天空,很多VR体验都是从一个角度出发的。

映射的代码如下:

float2((1 + atan2(InVector.x, - InVector.y) / 3.14159265) / 2, acos(InVector.z) / 3.14159265);

球形多重投影着色器算法如下:

  • 对于每个像素:

    • 根据探针的深度立方图测试可见性。如果探针可见,则将其穿过计分系统。其中可见性测试过程:

      • 每个探测器都包含从其位置渲染的深度立方体贴图。离线渲染。
      • Unity不支持用于存储深度的更高精度立方体贴图格式。
      • 使用不同的32位浮点编码函数。大多数情况下,可尝试在值中引入了太多的不稳定性,当你远离探测器时,误差会变得更大,增加一个小偏差,随着距离的增加略有增加。
      • 从未测试存储深度值的拉特朗(latlong)纹理。可能会解决cubemaps的部分或全部问题。

      探针可见性视图模式有助于放置探针。

    • 最佳探头的投影颜色。还可以添加等级库和反射。

    • 如果没有找到探测器,可以回退到单一的颜色,使用全局指定探测器的颜色,使用顶点颜色选择要使用的探测器。

以上技术看起来很棒,开销较低,最昂贵的是每只眼睛渲染大约0.8毫秒。意味着可以用其他很酷的东西来扩展这项技术,可能会占用大量内存,小几何体可能会有问题,因为深度立方体贴图的分辨率不够高(通常约512)。

高质量的移动虚拟现实(VR)是即将到来的图形技术时代的要求:世界各地的用户,无论其硬件和网络条件如何,都可以享受沉浸式的虚拟体验。然而,由于用户行为的高度交互性和VR执行过程中复杂的环境约束,基于最先进软件的移动VR设计无法完全满足实时性能要求。受独特的人类视觉系统效果以及VR运动特征与实时硬件级别信息之间的强相关性的启发,Q-VR: System-Level Design for Future Mobile Collaborative Virtual Reality提出了Q-VR,这是一种通过软硬件协同设计实现未来低延迟高质量移动VR的新型动态协同渲染解决方案。在软件层面,Q-VR提供灵活的高级调整界面,以减少网络延迟,同时保持用户感知。在硬件层面,Q-VR通过有效利用日益强大的VR硬件的计算能力,适应了用户广泛的硬件和网络条件。对真实游戏的广泛评估表明,Q-VR可以达到平均水平与商用VR设备中的传统本地渲染设计相比,端到端性能提高了3.4倍(高达6.7倍),与最先进的静态协同渲染相比,帧速率提高了4.1倍。

Q-VR的软硬件代码设计的处理图。

现代VR图形管线示例。

在当前两种移动VR系统设计上运行高端VR应用程序时的系统延迟和FPS。

静态协同渲染的执行管线和Q-VR,Q-VR的软件和硬件优化反映在管线上。渲染任务在概念上映射到不同的硬件组件,其中LIWC和UCA是该文新设计的。由于多加速器并行,帧内任务可能会实时重叠(例如RR、网络和VD)。CL:软件控制逻辑;LS:本地设置;LR:局部渲染;C: 组成;RR:远程渲染;VD:视频解码;LIWC:轻量级交互感知工作负载控制器;UCA:统一合成和ATW。

视觉感知启发Q-VR中的软件级设置和配置示例,其编程模型,以及它如何与硬件接口。

该文提议的LIWC架构图。

基线顺序执行和统一合成与ATW(UCA)之间的比较。

UCA架构图。

Towards a Better Understanding of VR Sickness通过评估VR疾病的身体症状水平来解决VR疾病评估(VRSA)的黑盒问题。对于诱发类似VR疾病级别的VR内容,身体症状可能会因内容的特征而异。现有的VRSA方法大多侧重于评估VR疾病的总体评分。为了更好地了解VR疾病,需要预测和提供VR病的主要症状水平,而不是VR病的总体程度。该文预测了影响VR疾病总体程度的主要身体症状的程度,即定向障碍、恶心和动眼神经。此外,还为VRSA引入了一个新的大规模数据集,包括360个具有不同帧率、生理信号和主观评分的视频。在VRSA基准测试和我们新收集的数据集上,我们的方法不仅有可能实现与主观得分的最高相关性,而且有可能更好地了解哪些症状是VR疾病的主要原因。

身体症状预测的直觉有助于更好地理解VR疾病。一般来说,VR内容根据其时空特征导致不同程度的身体症状。

考虑神经失配机制的身体症状预测说明。

该文提出了一种新的客观身体症状预测方法,以更好地理解VR疾病,解决了现有工作中没有考虑身体症状的局限性。此外,构建了80个具有四种不同帧率的360度视频,并进行了广泛的主观实验,以获得生理信号(HR和GSR)和身体症状评分的主观问卷(SSQ分数)。在广泛的实验中,证明了该模型不仅可以提供VR疾病的总体评分,还可以提供VR病的身体症状。这可以作为查看VR内容安全性的实际应用。

A Study of Networking Performance in a Multi-user VR Environment探讨了VR中的多人互动实现和优化技术。

多人VR的一种CS架构。在此体系结构中,服务器可以控制应用程序的所有方面,例如向连接的客户端传输数据。在此上下文中,服务器是客户端可以连接到的游戏实例,客户端也是游戏实例。主要区别在于客户端对场景中的网络对象没有权限,意味着客户端无法更新其场景中其他对象的更改。由于服务器拥有对所有网络对象的权限,因此它负责更新场景中所有更改的连接客户端,并处理传入的请求。客户端仍然可以在本地对其场景进行更改,而不会通知任何其他人。它在许多情况下都很有用,例如处理控制器输入和设置播放器摄像头。

Virtual Hands in VR: Motion Capture, Synthesis, and Perception信息深入地阐述了VR中的动捕、合成和感知相关的技术,感兴趣的童鞋不容错过。[QuickTime VR – An Image-Based Approach to Virtual Environment Navigation](QuickTime VR – An Image-Based Approach to Virtual Environment Navigation.pdf)阐述了一种基于图像的虚拟环境导航方法

——QuickTimeVR。Capture, Reconstruction, and Representation of the Visual Real World for Virtual Reality解析了VR中的动捕、重建和表达等技术。High-Fidelity Facial and Speech Animation for VR HMDs解析了VR头显中的高保真面部和语音动画捕捉和重建。[Temporally Adaptive Shading Reuse for Real-Time Rendering and Virtual Reality](Temporally Adaptive Shading Reuse for Real-Time Rendering and Virtual Reality.pdf)分享了用于实时渲染和虚拟现实的时间自适应着色重用的技术。此外,有些公司(如华为)在研究基于云渲染的VR架构:

15.3 UE XR

早在UE3时代,就已经通过节点图支持了VR的渲染,其中渲染管线的不同组件可以在多个配置中重新排列、修改和重新连接。根据所支持的节点类型,有时可以以产生特定VR技术效果的方式配置节点。下图显示了一个示例,它描述了虚幻引擎的材质编辑界面,该界面被配置为渲染红色青色立体图像作为后处理效果。

使用UE3的“材质编辑器”(Material Editor)配置虚幻引擎以支持红青色立体感,可以以这种方式支持其它立体编码,例如通过隔行扫描用于偏振立体显示的图像。

以下是2013前后的游戏引擎对VR的支持情况表:

时至今日,UE4.27及之后的版本已经支持AR、VR、MR等技术,支持Google、Apple、微软、Maigic Leap、Oculus、SteamVR、三星等公司及其旗下的众多XR平台,当然也包括OpenXR等标准接口。

剖析虚幻渲染体系(12)- 移动端专题Part 1(UE移动端渲染分析)已经详细地剖析过UE的移动端源码,顺带分析了XR的部分渲染技术。下面针对XR的某些要点渲染进行剖析。本节以UE 4.27.2为剖析的蓝本。

15.3.2.1 Multi-View

UE的Multi-View可由下面界面设置开启或关闭:

在代码中,由控制台变量vr.MobileMultiView保存其值,而涉及到该控制台变量的主要代码如下:

// MobileShadingRenderer.cpp

FRHITexture* FMobileSceneRenderer::RenderForward(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> ViewList)
{
    (...)

    // 获取控制台变量
    static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView"));
    const bool bIsMultiViewApplication = (CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0);

    (...)

    // 如果scenecolor不是多视图,但应用程序是多视图,则需要由于着色器而渲染为单视图多视图。
    SceneColorRenderPassInfo.MultiViewCount = View.bIsMobileMultiViewEnabled ? 2 : (bIsMultiViewApplication ? 1 : 0);

    (...)
}

// VulkanRenderTarget.cpp

FVulkanRenderTargetLayout::FVulkanRenderTargetLayout(const FGraphicsPipelineStateInitializer& Initializer)
{
    (...)

    FRenderPassCompatibleHashableStruct CompatibleHashInfo;

    (...)

    MultiViewCount = Initializer.MultiViewCount;

    (...)

    CompatibleHashInfo.MultiViewCount = MultiViewCount;

    (...)
}

// VulkanRHI.cpp

static VkRenderPass CreateRenderPass(FVulkanDevice& InDevice, const FVulkanRenderTargetLayout& RTLayout)
{
    (...)

    // 0b11 for 2, 0b1111 for 4, and so on
    uint32 MultiviewMask = ( 0b1 << RTLayout.GetMultiViewCount() ) - 1;

    (...)

    const uint32_t ViewMask[2] = { MultiviewMask, MultiviewMask };
    const uint32_t CorrelationMask = MultiviewMask;

    VkRenderPassMultiviewCreateInfo MultiviewInfo;
    if (RTLayout.GetIsMultiView())
    {
        FMemory::Memzero(MultiviewInfo);
        MultiviewInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_MULTIVIEW_CREATE_INFO;
        MultiviewInfo.pNext = nullptr;
        MultiviewInfo.subpassCount = NumSubpasses;
        MultiviewInfo.pViewMasks = ViewMask;
        MultiviewInfo.dependencyCount = 0;
        MultiviewInfo.pViewOffsets = nullptr;
        MultiviewInfo.correlationMaskCount = 1;
        MultiviewInfo.pCorrelationMasks = &CorrelationMask;

        CreateInfo.pNext = &MultiviewInfo;
    }

    (...)
}

以上是针对Vulkan图形API的处理,对于OpenGL ES,具体教程可参考Using multiview rendering,在UE也是另外的处理代码:

// OpenGLES.cpp

void FOpenGLES::ProcessExtensions(const FString& ExtensionsString)
{
    (...)

    // 检测是否支持Multi-View扩展
    const bool bMultiViewSupport = ExtensionsString.Contains(TEXT("GL_OVR_multiview"));
    const bool bMultiView2Support = ExtensionsString.Contains(TEXT("GL_OVR_multiview2"));
    const bool bMultiViewMultiSampleSupport = ExtensionsString.Contains(TEXT("GL_OVR_multiview_multisampled_render_to_texture"));
    if (bMultiViewSupport && bMultiView2Support && bMultiViewMultiSampleSupport)
    {
        glFramebufferTextureMultiviewOVR = (PFNGLFRAMEBUFFERTEXTUREMULTIVIEWOVRPROC)((void*)eglGetProcAddress("glFramebufferTextureMultiviewOVR"));
        glFramebufferTextureMultisampleMultiviewOVR = (PFNGLFRAMEBUFFERTEXTUREMULTISAMPLEMULTIVIEWOVRPROC)((void*)eglGetProcAddress("glFramebufferTextureMultisampleMultiviewOVR"));

        bSupportsMobileMultiView = (glFramebufferTextureMultiviewOVR != NULL) && (glFramebufferTextureMultisampleMultiviewOVR != NULL);
    }

    (...)
}

// OpenGLES.h

struct FOpenGLES : public FOpenGLBase
{
    (...)

    static FORCEINLINE bool SupportsMobileMultiView() { return bSupportsMobileMultiView; }

    (...)
}

// OpenGLDevice.cpp

static void InitRHICapabilitiesForGL()
{
    (...)

    GSupportsMobileMultiView = FOpenGL::SupportsMobileMultiView();

    (...)
}

// OpenGLRenderTarget.cpp

GLuint FOpenGLDynamicRHI::GetOpenGLFramebuffer(uint32 NumSimultaneousRenderTargets, FOpenGLTextureBase** RenderTargets, const uint32* ArrayIndices, const uint32* MipmapLevels, FOpenGLTextureBase* DepthStencilTarget)
{
    (...)

if PLATFORM_ANDROID && !PLATFORM_LUMINGL4
    static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView"));

    // 如果启用并支持,请分配移动多视图帧缓冲区。
    // 多视图不支持读取缓冲区,显式禁用并仅绑定GL_DRAW_FRAMEBUFFER.
    const bool bRenderTargetsDefined = (RenderTargets != nullptr) && RenderTargets[0];
    const bool bValidMultiViewDepthTarget = !DepthStencilTarget || DepthStencilTarget->Target == GL_TEXTURE_2D_ARRAY;
    const bool bUsingArrayTextures = (bRenderTargetsDefined) ? (RenderTargets[0]->Target == GL_TEXTURE_2D_ARRAY && bValidMultiViewDepthTarget) : false;
    const bool bMultiViewCVar = CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0;

    if (bUsingArrayTextures && FOpenGL::SupportsMobileMultiView() && bMultiViewCVar)
    {
        FOpenGLTextureBase* const RenderTarget = RenderTargets[0];
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, Framebuffer);

        FOpenGLTexture2D* RenderTarget2D = (FOpenGLTexture2D*)RenderTarget;
        const uint32 NumSamplesTileMem = RenderTarget2D->GetNumSamplesTileMem();
        if (NumSamplesTileMem > 1)
        {
            glFramebufferTextureMultisampleMultiviewOVR(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, RenderTarget->GetResource(), 0, NumSamplesTileMem, 0, 2);
            VERIFY_GL(glFramebufferTextureMultisampleMultiviewOVR);

            if (DepthStencilTarget)
            {
                glFramebufferTextureMultisampleMultiviewOVR(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, DepthStencilTarget->GetResource(), 0, NumSamplesTileMem, 0, 2);
                VERIFY_GL(glFramebufferTextureMultisampleMultiviewOVR);
            }
        }
        else
        {
            glFramebufferTextureMultiviewOVR(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, RenderTarget->GetResource(), 0, 0, 2);
            VERIFY_GL(glFramebufferTextureMultiviewOVR);

            if (DepthStencilTarget)
            {
                glFramebufferTextureMultiviewOVR(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, DepthStencilTarget->GetResource(), 0, 0, 2);
                VERIFY_GL(glFramebufferTextureMultiviewOVR);
            }
        }

        FOpenGL::CheckFrameBuffer();

        FOpenGL::ReadBuffer(GL_NONE);
        FOpenGL::DrawBuffer(GL_COLOR_ATTACHMENT0);

        GetOpenGLFramebufferCache().Add(FOpenGLFramebufferKey(NumSimultaneousRenderTargets, RenderTargets, ArrayIndices, MipmapLevels, DepthStencilTarget, PlatformOpenGLCurrentContext(PlatformDevice)), Framebuffer + 1);

        return Framebuffer;
    }
#endif

    (...)
}

对应的Shader代码需要添加相应的关键字或语句:

// OpenGLShaders.cpp

void OPENGLDRV_API GLSLToDeviceCompatibleGLSL(...)
{
    // Whether we need to emit mobile multi-view code or not.
    const bool bEmitMobileMultiView = (FCStringAnsi::Strstr(GlslCodeOriginal.GetData(), "gl_ViewID_OVR") != nullptr);

    (...)

    if (bEmitMobileMultiView)
    {
        MoveHashLines(GlslCode, GlslCodeOriginal);

        if (GSupportsMobileMultiView)
        {
            AppendCString(GlslCode, "\n\n");
            AppendCString(GlslCode, "#extension GL_OVR_multiview2 : enable\n");
            AppendCString(GlslCode, "\n\n");
        }
        else
        {
            // Strip out multi-view for devices that don't support it.
            AppendCString(GlslCode, "#define gl_ViewID_OVR 0\n");
        }
    }

    (...)
}

void OPENGLDRV_API GLSLToDeviceCompatibleGLSL(...)
{
    (...)

    if (bEmitMobileMultiView && GSupportsMobileMultiView && TypeEnum == GL_VERTEX_SHADER)
    {
        AppendCString(GlslCode, "\n\n");
        AppendCString(GlslCode, "layout(num_views = 2) in;\n");
        AppendCString(GlslCode, "\n\n");
    }

    (...)
}

在shader代码中,用MOBILE_MULTI_VIEW指定是否启用了移动端多视图:

// MobileBasePassVertexShader.usf

void Main(
    FVertexFactoryInput Input
    , out FMobileShadingBasePassVSOutput Output
#if INSTANCED_STEREO
    , uint InstanceId : SV_InstanceID
    , out uint LayerIndex : SV_RenderTargetArrayIndex
#elif MOBILE_MULTI_VIEW
    // 表明了移动端多视图的视图索引。
    , in uint ViewId : SV_ViewID
#endif
    )
{
    (...)
#elif MOBILE_MULTI_VIEW
    // 根据ViewId解析视图,获得解析后的结果。
    #if COMPILER_GLSL_ES3_1
        const int MultiViewId = int(ViewId);
        ResolvedView = ResolveView(uint(MultiViewId));
        Output.BasePassInterpolants.MultiViewId = float(MultiViewId);
    #else
        ResolvedView = ResolveView(ViewId);
        Output.BasePassInterpolants.MultiViewId = float(ViewId);
    #endif
#else
    (...)
}

// InstancedStereo.ush

ViewState ResolveView(uint ViewIndex)
{
    if (ViewIndex == 0)
    {
        return GetPrimaryView();
    }
    else
    {
        return GetInstancedView();
    }
}

// ShaderCompiler.cpp

ENGINE_API void GenerateInstancedStereoCode(FString& Result, EShaderPlatform ShaderPlatform)
{
    (...)

    // 定义ViewState
    Result =  "struct ViewState\r\n";
    Result += "{\r\n";
    for (int32 MemberIndex = 0; MemberIndex < StructMembers.Num(); ++MemberIndex)
    {
        const FShaderParametersMetadata::FMember& Member = StructMembers[MemberIndex];
        FString MemberDecl;
        GenerateUniformBufferStructMember(MemberDecl, StructMembers[MemberIndex], ShaderPlatform);
        Result += FString::Printf(TEXT("\t%s;\r\n"), *MemberDecl);
    }
    Result += "};\r\n";

    // 定义GetPrimaryView
    Result += "ViewState GetPrimaryView()\r\n";
    Result += "{\r\n";
    Result += "\tViewState Result;\r\n";
    for (int32 MemberIndex = 0; MemberIndex < StructMembers.Num(); ++MemberIndex)
    {
        const FShaderParametersMetadata::FMember& Member = StructMembers[MemberIndex];
        Result += FString::Printf(TEXT("\tResult.%s = View.%s;\r\n"), Member.GetName(), Member.GetName());
    }
    Result += "\treturn Result;\r\n";
    Result += "}\r\n";

    // 定义GetInstancedView
    Result += "ViewState GetInstancedView()\r\n";
    Result += "{\r\n";
    Result += "\tViewState Result;\r\n";
    for (int32 MemberIndex = 0; MemberIndex < StructMembers.Num(); ++MemberIndex)
    {
        const FShaderParametersMetadata::FMember& Member = StructMembers[MemberIndex];
        Result += FString::Printf(TEXT("\tResult.%s = InstancedView.%s;\r\n"), Member.GetName(), Member.GetName());
    }
    Result += "\treturn Result;\r\n";
    Result += "}\r\n";

    (...)
}

15.3.2.2 Fixed Foveation

固定注视点渲染也可以在UE的工程设置的VR页面中开启,对应的控制台变量是vr.VRS.HMDFixedFoveationLevel。UE相关的处理代码如下:

// VariableRateShadingImageManager.cpp

FRDGTextureRef FVariableRateShadingImageManager::GetVariableRateShadingImage(FRDGBuilder& GraphBuilder, const FSceneViewFamily& ViewFamily, const TArray<TRefCountPtr<IPooledRenderTarget>>* ExternalVRSSources, EVRSType VRSTypesToExclude)
{
    // 如果RHI不支持VRS,应该立即返回。
    if (!GRHISupportsAttachmentVariableRateShading || !GRHIVariableRateShadingEnabled || !GRHIAttachmentVariableRateShadingEnabled)
    {
        return nullptr;
    }

    // 始终要确保更新每一帧,即使不会生成任何VRS图像。
    Tick();

    if (EnumHasAllFlags(VRSTypesToExclude, EVRSType::All))
    {
        return nullptr;
    }

    FVRSImageGenerationParameters VRSImageParams;

    const bool bIsStereo = IStereoRendering::IsStereoEyeView(*ViewFamily.Views[0]) && GEngine->XRSystem.IsValid();

    VRSImageParams.bInstancedStereo |= ViewFamily.Views[0]->IsInstancedStereoPass();
    VRSImageParams.Size = FIntPoint(ViewFamily.RenderTarget->GetSizeXY());

    UpdateFixedFoveationParameters(VRSImageParams);
    UpdateEyeTrackedFoveationParameters(VRSImageParams, ViewFamily);

    EVRSGenerationFlags GenFlags = EVRSGenerationFlags::None;

    // 设置XR foveation VRS生成的生成标志。
    if (bIsStereo && !EnumHasAnyFlags(VRSTypesToExclude, EVRSType::XRFoveation) && !EnumHasAnyFlags(VRSTypesToExclude, EVRSType::EyeTrackedFoveation))
    {
        EnumAddFlags(GenFlags, EVRSGenerationFlags::StereoRendering);

        if (!EnumHasAnyFlags(VRSTypesToExclude, EVRSType::FixedFoveation) && VRSImageParams.bGenerateFixedFoveation)
        {
            EnumAddFlags(GenFlags, EVRSGenerationFlags::HMDFixedFoveation);
        }

        if (!EnumHasAllFlags(VRSTypesToExclude, EVRSType::EyeTrackedFoveation) && VRSImageParams.bGenerateEyeTrackedFoveation)
        {
            EnumAddFlags(GenFlags, EVRSGenerationFlags::HMDEyeTrackedFoveation);
        }

        if (VRSImageParams.bInstancedStereo)
        {
            EnumAddFlags(GenFlags, EVRSGenerationFlags::SideBySideStereo);
        }
    }

    if (GenFlags == EVRSGenerationFlags::None)
    {
        if (ExternalVRSSources == nullptr || ExternalVRSSources->Num() == 0)
        {
            // Nothing to generate.
            return nullptr;
        }
        else
        {
            // If there's one external VRS image, just return that since we're not building anything here.
            if (ExternalVRSSources->Num() == 1)
            {
                const FIntVector& ExtSize = (*ExternalVRSSources)[0]->GetDesc().GetSize();
                check(ExtSize.X == VRSImageParams.Size.X / GRHIVariableRateShadingImageTileMinWidth && ExtSize.Y == VRSImageParams.Size.Y / GRHIVariableRateShadingImageTileMinHeight);
                return GraphBuilder.RegisterExternalTexture((*ExternalVRSSources)[0]);
            }

            // If there is more than one external image, we'll generate a final one by combining, so fall through.
        }
    }

    // 获取FOV
    IHeadMountedDisplay* HMDDevice = (GEngine->XRSystem == nullptr) ? nullptr : GEngine->XRSystem->GetHMDDevice();
    if (HMDDevice != nullptr)
    {
        HMDDevice->GetFieldOfView(VRSImageParams.HMDFieldOfView.X, VRSImageParams.HMDFieldOfView.Y);
    }

    const uint64 Key = CalculateVRSImageHash(VRSImageParams, GenFlags);
    FActiveTarget* ActiveTarget = ActiveVRSImages.Find(Key);
    if (ActiveTarget == nullptr)
    {
        // 渲染VRS
        return GraphBuilder.RegisterExternalTexture(RenderShadingRateImage(GraphBuilder, Key, VRSImageParams, GenFlags));
    }

    ActiveTarget->LastUsedFrame = GFrameNumber;

    return GraphBuilder.RegisterExternalTexture(ActiveTarget->Target);
}

// 渲染PC端的VRS
TRefCountPtr<IPooledRenderTarget> FVariableRateShadingImageManager::RenderShadingRateImage(...)
{
    (...)
}

// 渲染移动端的VRS
TRefCountPtr<IPooledRenderTarget> FVariableRateShadingImageManager::GetMobileVariableRateShadingImage(const FSceneViewFamily& ViewFamily)
{
    if (!(IStereoRendering::IsStereoEyeView(*ViewFamily.Views[0]) && GEngine->XRSystem.IsValid()))
    {
        return TRefCountPtr<IPooledRenderTarget>();
    }

    FIntPoint Size(ViewFamily.RenderTarget->GetSizeXY());

    const bool bStereo = GEngine->StereoRenderingDevice.IsValid() && GEngine->StereoRenderingDevice->IsStereoEnabled();
    IStereoRenderTargetManager* const StereoRenderTargetManager = bStereo ? GEngine->StereoRenderingDevice->GetRenderTargetManager() : nullptr;

    FTexture2DRHIRef Texture;
    FIntPoint TextureSize(0, 0);

    // 如果支持,为VR注视点分配可变分辨率纹理。
    if (StereoRenderTargetManager && StereoRenderTargetManager->NeedReAllocateShadingRateTexture(MobileHMDFixedFoveationOverrideImage))
    {
        bool bAllocatedShadingRateTexture = StereoRenderTargetManager->AllocateShadingRateTexture(0, Size.X, Size.Y, GRHIVariableRateShadingImageFormat, 0, TexCreate_None, TexCreate_None, Texture, TextureSize);
        if (bAllocatedShadingRateTexture)
        {
            MobileHMDFixedFoveationOverrideImage = CreateRenderTarget(Texture, TEXT("ShadingRate"));
        }
    }

    return MobileHMDFixedFoveationOverrideImage;
}

shader代码如下:

// VariableRateShading.usf

(...)

uint GetFoveationShadingRate(float FractionalOffset, float FullCutoffSquared, float HalfCutoffSquared)
{
    if (FractionalOffset > HalfCutoffSquared)
    {
        return SHADING_RATE_4x4;
    }

    if (FractionalOffset > FullCutoffSquared)
    {
        return SHADING_RATE_2x2;
    }

    return SHADING_RATE_1x1;
}

uint GetFixedFoveationRate(uint2 PixelPositionIn)
{
    const float2 PixelPosition = float2((float)PixelPositionIn.x, (float)PixelPositionIn.y);
    const float FractionalOffset = GetFractionalOffsetFromEyeOrigin(PixelPosition);
    return GetFoveationShadingRate(FractionalOffset, FixedFoveationFullRateCutoffSquared, FixedFoveationHalfRateCutoffSquared);
}

uint GetEyetrackedFoveationRate(uint2 PixelPositionIn)
{
    return SHADING_RATE_1x1;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Return the ideal combination of two specified shading rate values.
////////////////////////////////////////////////////////////////////////////////////////////////////

// 组合两个着色率,其实就是取大的那个。
uint CombineShadingRates(uint Rate1, uint Rate2)
{
    return max(Rate1, Rate2);
}

// 生成着色率纹理
[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, 1)]
void GenerateShadingRateTexture(uint3 DispatchThreadId : SV_DispatchThreadID)
{
    const uint2 TexelCoord = DispatchThreadId.xy;
    uint ShadingRateOut = 0;

    if ((ShadingRateAttachmentGenerationFlags & HMD_FIXED_FOVEATION) != 0)
    {
        ShadingRateOut = CombineShadingRates(ShadingRateOut, GetFixedFoveationRate(TexelCoord));
    }

    if ((ShadingRateAttachmentGenerationFlags & HMD_EYETRACKED_FOVEATION) != 0)
    {
        ShadingRateOut = CombineShadingRates(ShadingRateOut, GetEyetrackedFoveationRate(TexelCoord));
    }

    // Conservative combination, just return the max of the two.
    RWOutputTexture[TexelCoord] = ShadingRateOut;
}

由此可知,实现固定注视点需要借助VRS的特性。

15.3.2.3 OpenXR

OpenXR是UE内置的插件,可在插件界面中搜索并开启:

OpenXR的插件代码在:Engine\Plugins\Runtime\OpenXR。OpenXR的标准接口如下:

// OpenXRCore.h

/** List all OpenXR global entry points used by Unreal. */
#define ENUM_XR_ENTRYPOINTS_GLOBAL(EnumMacro) \
    EnumMacro(PFN_xrEnumerateApiLayerProperties,xrEnumerateApiLayerProperties) \
    EnumMacro(PFN_xrEnumerateInstanceExtensionProperties,xrEnumerateInstanceExtensionProperties) \
    EnumMacro(PFN_xrCreateInstance,xrCreateInstance)

/** List all OpenXR instance entry points used by Unreal. */
#define ENUM_XR_ENTRYPOINTS(EnumMacro) \
    EnumMacro(PFN_xrDestroyInstance,xrDestroyInstance) \
    EnumMacro(PFN_xrGetInstanceProperties,xrGetInstanceProperties) \
    EnumMacro(PFN_xrPollEvent,xrPollEvent) \
    EnumMacro(PFN_xrResultToString,xrResultToString) \
    EnumMacro(PFN_xrStructureTypeToString,xrStructureTypeToString) \
    EnumMacro(PFN_xrGetSystem,xrGetSystem) \
    EnumMacro(PFN_xrGetSystemProperties,xrGetSystemProperties) \
    EnumMacro(PFN_xrEnumerateEnvironmentBlendModes,xrEnumerateEnvironmentBlendModes) \
    EnumMacro(PFN_xrCreateSession,xrCreateSession) \
    EnumMacro(PFN_xrDestroySession,xrDestroySession) \
    EnumMacro(PFN_xrEnumerateReferenceSpaces,xrEnumerateReferenceSpaces) \
    EnumMacro(PFN_xrCreateReferenceSpace,xrCreateReferenceSpace) \
    EnumMacro(PFN_xrGetReferenceSpaceBoundsRect,xrGetReferenceSpaceBoundsRect) \
    EnumMacro(PFN_xrCreateActionSpace,xrCreateActionSpace) \
    EnumMacro(PFN_xrLocateSpace,xrLocateSpace) \
    EnumMacro(PFN_xrDestroySpace,xrDestroySpace) \
    EnumMacro(PFN_xrEnumerateViewConfigurations,xrEnumerateViewConfigurations) \
    EnumMacro(PFN_xrGetViewConfigurationProperties,xrGetViewConfigurationProperties) \
    EnumMacro(PFN_xrEnumerateViewConfigurationViews,xrEnumerateViewConfigurationViews) \
    EnumMacro(PFN_xrEnumerateSwapchainFormats,xrEnumerateSwapchainFormats) \
    EnumMacro(PFN_xrCreateSwapchain,xrCreateSwapchain) \
    EnumMacro(PFN_xrDestroySwapchain,xrDestroySwapchain) \
    EnumMacro(PFN_xrEnumerateSwapchainImages,xrEnumerateSwapchainImages) \
    EnumMacro(PFN_xrAcquireSwapchainImage,xrAcquireSwapchainImage) \
    EnumMacro(PFN_xrWaitSwapchainImage,xrWaitSwapchainImage) \
    EnumMacro(PFN_xrReleaseSwapchainImage,xrReleaseSwapchainImage) \
    EnumMacro(PFN_xrBeginSession,xrBeginSession) \
    EnumMacro(PFN_xrEndSession,xrEndSession) \
    EnumMacro(PFN_xrRequestExitSession,xrRequestExitSession) \
    EnumMacro(PFN_xrWaitFrame,xrWaitFrame) \
    EnumMacro(PFN_xrBeginFrame,xrBeginFrame) \
    EnumMacro(PFN_xrEndFrame,xrEndFrame) \
    EnumMacro(PFN_xrLocateViews,xrLocateViews) \
    EnumMacro(PFN_xrStringToPath,xrStringToPath) \
    EnumMacro(PFN_xrPathToString,xrPathToString) \
    EnumMacro(PFN_xrCreateActionSet,xrCreateActionSet) \
    EnumMacro(PFN_xrDestroyActionSet,xrDestroyActionSet) \
    EnumMacro(PFN_xrCreateAction,xrCreateAction) \
    EnumMacro(PFN_xrDestroyAction,xrDestroyAction) \
    EnumMacro(PFN_xrSuggestInteractionProfileBindings,xrSuggestInteractionProfileBindings) \
    EnumMacro(PFN_xrAttachSessionActionSets,xrAttachSessionActionSets) \
    EnumMacro(PFN_xrGetCurrentInteractionProfile,xrGetCurrentInteractionProfile) \
    EnumMacro(PFN_xrGetActionStateBoolean,xrGetActionStateBoolean) \
    EnumMacro(PFN_xrGetActionStateFloat,xrGetActionStateFloat) \
    EnumMacro(PFN_xrGetActionStateVector2f,xrGetActionStateVector2f) \
    EnumMacro(PFN_xrGetActionStatePose,xrGetActionStatePose) \
    EnumMacro(PFN_xrSyncActions,xrSyncActions) \
    EnumMacro(PFN_xrEnumerateBoundSourcesForAction,xrEnumerateBoundSourcesForAction) \
    EnumMacro(PFN_xrGetInputSourceLocalizedName,xrGetInputSourceLocalizedName) \
    EnumMacro(PFN_xrApplyHapticFeedback,xrApplyHapticFeedback) \
    EnumMacro(PFN_xrStopHapticFeedback,xrStopHapticFeedback)

完成的OpenXR接口参见XR Spec。UE中涉及的重要类型和接口如下:

// OpenXRAR.h

// OpenXR系统
class FOpenXRARSystem :
    public FARSystemSupportBase,
    public IOpenXRARTrackedMeshHolder,
    public IOpenXRARTrackedGeometryHolder,
    public FGCObject,
    public TSharedFromThis<FOpenXRARSystem, ESPMode::ThreadSafe>
{
public:
    FOpenXRARSystem();
    virtual ~FOpenXRARSystem();

    void SetTrackingSystem(TSharedPtr<FXRTrackingSystemBase, ESPMode::ThreadSafe> InTrackingSystem);

    virtual void OnARSystemInitialized();
    virtual bool OnStartARGameFrame(FWorldContext& WorldContext);

    virtual void OnStartARSession(UARSessionConfig* SessionConfig);
    virtual void OnPauseARSession();
    virtual void OnStopARSession();
    virtual FARSessionStatus OnGetARSessionStatus() const;
    virtual bool OnIsSessionTrackingFeatureSupported(EARSessionType SessionType, EARSessionTrackingFeature SessionTrackingFeature) const;

    (...)

private:
    // FOpenXRHMD实例
    FOpenXRHMD* TrackingSystem;

    class IOpenXRCustomAnchorSupport* CustomAnchorSupport = nullptr;
    FARSessionStatus SessionStatus;

    class IOpenXRCustomCaptureSupport* QRCapture = nullptr;
    class IOpenXRCustomCaptureSupport* CamCapture = nullptr;
    class IOpenXRCustomCaptureSupport* SpatialMappingCapture = nullptr;
    class IOpenXRCustomCaptureSupport* SceneUnderstandingCapture = nullptr;
    class IOpenXRCustomCaptureSupport* HandMeshCapture = nullptr;

    TArray<IOpenXRCustomCaptureSupport*> CustomCaptureSupports;

    (...)
};

// IHeadMountedDisplayModule.h

// 头戴式显示模块的公共接口.
class IHeadMountedDisplayModule : public IModuleInterface, public IModularFeature
{
public:
    static FName GetModularFeatureName();
    virtual FString GetModuleKeyName() const = 0;
    virtual void GetModuleAliases(TArray<FString>& AliasesOut) const;
    float GetModulePriority() const;

    static inline IHeadMountedDisplayModule& Get();
    static inline bool IsAvailable();

    virtual void StartupModule() override;
    virtual bool PreInit();
    virtual bool IsHMDConnected();

    virtual uint64 GetGraphicsAdapterLuid();

    virtual FString GetAudioInputDevice();
    virtual FString GetAudioOutputDevice();

    virtual TSharedPtr< class IXRTrackingSystem, ESPMode::ThreadSafe > CreateTrackingSystem() = 0;
    virtual TSharedPtr< IHeadMountedDisplayVulkanExtensions, ESPMode::ThreadSafe > GetVulkanExtensions();
    virtual bool IsStandaloneStereoOnlyDevice();
};

// IOpenXRHMDPlugin.h

// 此模块的公共接口。在大多数情况下,此接口仅对该插件中的同级模块公开。
class OPENXRHMD_API IOpenXRHMDPlugin : public IHeadMountedDisplayModule
{
public:
    static inline IOpenXRHMDPlugin& Get()
    {
        return FModuleManager::LoadModuleChecked< IOpenXRHMDPlugin >( "OpenXRHMD" );
    }

    static inline bool IsAvailable();

    virtual bool IsExtensionAvailable(const FString& Name) const = 0;
    virtual bool IsExtensionEnabled(const FString& Name) const = 0;

    virtual bool IsLayerAvailable(const FString& Name) const = 0;
    virtual bool IsLayerEnabled(const FString& Name) const = 0;
};

// OpenXRHMD.cpp

class FOpenXRHMDPlugin : public IOpenXRHMDPlugin
{
public:
    FOpenXRHMDPlugin();
    ~FOpenXRHMDPlugin();

    // 创建追踪系统(FOpenXRHMD实例)
    virtual TSharedPtr< class IXRTrackingSystem, ESPMode::ThreadSafe > CreateTrackingSystem() override
    {
        if (!RenderBridge)
        {
            if (!InitRenderBridge())
            {
                return nullptr;
            }
        }
        // 加载IOpenXRARModule。
        auto ARModule = FModuleManager::LoadModulePtr<IOpenXRARModule>("OpenXRAR");
        // 创建AR系统。
        auto ARSystem = ARModule->CreateARSystem();

        // 创建FOpenXRHMD实例.
        auto OpenXRHMD = FSceneViewExtensions::NewExtension<FOpenXRHMD>(Instance, System, RenderBridge, EnabledExtensions, ExtensionPlugins, ARSystem);
        if (OpenXRHMD->IsInitialized())
        {
            // 初始化ARSystem.
            ARModule->SetTrackingSystem(OpenXRHMD);
            OpenXRHMD->GetARCompositionComponent()->InitializeARSystem();
            return OpenXRHMD;
        }

        return nullptr;
    }

    (...)

private:
    void *LoaderHandle;
    // XR系统句柄
    XrInstance Instance;
    XrSystemId System;
    TSet<FString> AvailableExtensions;
    TSet<FString> AvailableLayers;
    TArray<const char*> EnabledExtensions;
    TArray<const char*> EnabledLayers;
    // IOpenXRHMDPlugin
    TArray<IOpenXRExtensionPlugin*> ExtensionPlugins;
    TRefCountPtr<FOpenXRRenderBridge> RenderBridge;
    TSharedPtr< IHeadMountedDisplayVulkanExtensions, ESPMode::ThreadSafe > VulkanExtensions;

    // 初始化系统的各类接口
    bool InitRenderBridge();
    bool InitInstanceAndSystem();
    bool InitInstance();
    bool InitSystem();

    (...)
};

// XRTrackingSystemBase.h

class HEADMOUNTEDDISPLAY_API FXRTrackingSystemBase : public IXRTrackingSystem
{
public:
    FXRTrackingSystemBase(IARSystemSupport* InARImplementation);
    virtual ~FXRTrackingSystemBase();

    virtual bool DoesSupportPositionalTracking() const override { return false; }
    virtual bool HasValidTrackingPosition() override { return DoesSupportPositionalTracking(); }
    virtual uint32 CountTrackedDevices(EXRTrackedDeviceType Type = EXRTrackedDeviceType::Any) override;
    virtual bool IsTracking(int32 DeviceId) override;
    virtual bool GetTrackingSensorProperties(int32 DeviceId, FQuat& OutOrientation, FVector& OutPosition, FXRSensorProperties& OutSensorProperties) override;
    virtual EXRTrackedDeviceType GetTrackedDeviceType(int32 DeviceId) const override;

    virtual TSharedPtr< class IXRCamera, ESPMode::ThreadSafe > GetXRCamera(int32 DeviceId = HMDDeviceId) override;

    virtual bool GetRelativeEyePose(int32 DeviceId, EStereoscopicPass Eye, FQuat& OutOrientation, FVector& OutPosition) override;

    virtual void SetTrackingOrigin(EHMDTrackingOrigin::Type NewOrigin) override;
    virtual EHMDTrackingOrigin::Type GetTrackingOrigin() const override;
    virtual FTransform GetTrackingToWorldTransform() const override;
    virtual bool GetFloorToEyeTrackingTransform(FTransform& OutFloorToEye) const override;
    virtual void UpdateTrackingToWorldTransform(const FTransform& TrackingToWorldOverride) override;

    virtual void CalibrateExternalTrackingSource(const FTransform& ExternalTrackingTransform) override;
    virtual void UpdateExternalTrackingPosition(const FTransform& ExternalTrackingTransform) override;
    virtual class IXRLoadingScreen* GetLoadingScreen() override final;

    virtual void GetMotionControllerData(UObject* WorldContext, const EControllerHand Hand, FXRMotionControllerData& MotionControllerData) override;

    (...)

protected:
    TSharedPtr< class FDefaultXRCamera, ESPMode::ThreadSafe > XRCamera;
    FTransform CachedTrackingToWorld;
    FTransform CalibratedOffset;
    mutable class IXRLoadingScreen* LoadingScreen;

    (...)
};

// HeadMountedDisplayBase.h

class HEADMOUNTEDDISPLAY_API FHeadMountedDisplayBase : public FXRTrackingSystemBase, public IHeadMountedDisplay, public IStereoRendering
{
public:
    FHeadMountedDisplayBase(IARSystemSupport* InARImplementation);
    virtual ~FHeadMountedDisplayBase();

    virtual IStereoLayers* GetStereoLayers() override;

    virtual bool GetHMDDistortionEnabled(EShadingPath ShadingPath) const override;
    virtual void OnLateUpdateApplied_RenderThread(FRHICommandListImmediate& RHICmdList, const FTransform& NewRelativeTransform) override;

    virtual void CalculateStereoViewOffset(const enum EStereoscopicPass StereoPassType, FRotator& ViewRotation, const float WorldToMeters, FVector& ViewLocation) override;
    virtual void InitCanvasFromView(FSceneView* InView, UCanvas* Canvas) override;

    virtual bool IsSpectatorScreenActive() const override;

    virtual class ISpectatorScreenController* GetSpectatorScreenController() override;
    virtual class ISpectatorScreenController const* GetSpectatorScreenController() const override;

    virtual FVector2D GetEyeCenterPoint_RenderThread(EStereoscopicPass Eye) const;
    virtual FIntRect GetFullFlatEyeRect_RenderThread(FTexture2DRHIRef EyeTexture) const { return FIntRect(0, 0, 1, 1); }
    virtual void CopyTexture_RenderThread(FRHICommandListImmediate& RHICmdList, FRHITexture2D* SrcTexture, FIntRect SrcRect, FRHITexture2D* DstTexture, FIntRect DstRect, bool bClearBlack, bool bNoAlpha) const {}

    (...)

protected:
    mutable TSharedPtr<class FDefaultStereoLayers, ESPMode::ThreadSafe> DefaultStereoLayers;
    TUniquePtr<FDefaultSpectatorScreenController> SpectatorScreenController;

    (...)
};

// OpenXRHMD.h

// OpenXR头显接口。
class FOpenXRHMD
    : public FHeadMountedDisplayBase
    , public FXRRenderTargetManager
    , public FSceneViewExtensionBase
    , public FOpenXRAssetManager
    , public TStereoLayerManager<FOpenXRLayer>
{
public:
    virtual bool EnumerateTrackedDevices(TArray<int32>& OutDevices, EXRTrackedDeviceType Type = EXRTrackedDeviceType::Any) override;

    virtual bool GetRelativeEyePose(int32 InDeviceId, EStereoscopicPass InEye, FQuat& OutOrientation, FVector& OutPosition) override;
    virtual bool GetIsTracked(int32 DeviceId);

    // 获取HMD的当前姿态。
    virtual bool GetCurrentPose(int32 DeviceId, FQuat& CurrentOrientation, FVector& CurrentPosition) override;
    virtual bool GetPoseForTime(int32 DeviceId, FTimespan Timespan, FQuat& CurrentOrientation, FVector& CurrentPosition, bool& bProvidedLinearVelocity, FVector& LinearVelocity, bool& bProvidedAngularVelocity, FVector& AngularVelocityRadPerSec);
    virtual void SetBaseRotation(const FRotator& BaseRot) override;
    virtual FRotator GetBaseRotation() const override;

    virtual void SetBaseOrientation(const FQuat& BaseOrient) override;
    virtual FQuat GetBaseOrientation() const override;

    virtual void SetTrackingOrigin(EHMDTrackingOrigin::Type NewOrigin) override;
    virtual EHMDTrackingOrigin::Type GetTrackingOrigin() const override;

    (...)

public:
    FOpenXRHMD(const FAutoRegister&, XrInstance InInstance, XrSystemId InSystem, TRefCountPtr<FOpenXRRenderBridge>& InRenderBridge, TArray<const char*> InEnabledExtensions, TArray<class IOpenXRExtensionPlugin*> InExtensionPlugins, IARSystemSupport* ARSystemSupport);
    virtual ~FOpenXRHMD();

    // 开始RHI线程的渲染。
    void OnBeginRendering_RHIThread(const FPipelinedFrameState& InFrameState, FXRSwapChainPtr ColorSwapchain, FXRSwapChainPtr DepthSwapchain);
    // 结束RHI线程的渲染。
    void OnFinishRendering_RHIThread();

    (...)

private:
    TArray<const char*>        EnabledExtensions;
    TArray<class IOpenXRExtensionPlugin*> ExtensionPlugins;
    XrInstance                Instance;
    XrSystemId                System;

    // 渲染桥接器
    TRefCountPtr<FOpenXRRenderBridge> RenderBridge;
    // 渲染模块
    IRendererModule*        RendererModule;

    TArray<FHMDViewMesh>    HiddenAreaMeshes;
    TArray<FHMDViewMesh>    VisibleAreaMeshes;

    (...)
};

// OpenXRHMD_RenderBridge.h

// OpenXR渲染桥接器
class FOpenXRRenderBridge : public FXRRenderBridge
{
public:
    virtual void* GetGraphicsBinding() = 0;

     // 创建交换链。
    virtual FXRSwapChainPtr CreateSwapchain(...) = 0;
    FXRSwapChainPtr CreateSwapchain(...);

    // 呈现渲染的图像。
    virtual bool Present(int32& InOutSyncInterval) override
    {
        bool bNeedsNativePresent = true;

        if (OpenXRHMD)
        {
            OpenXRHMD->OnFinishRendering_RHIThread();
            bNeedsNativePresent = !OpenXRHMD->IsStandaloneStereoOnlyDevice();
        }

        InOutSyncInterval = 0; // VSync off

        return bNeedsNativePresent;
    }

    (...)

private:
    FOpenXRHMD* OpenXRHMD;
};

#ifdef XR_USE_GRAPHICS_API_D3D11
FOpenXRRenderBridge* CreateRenderBridge_D3D11(XrInstance InInstance, XrSystemId InSystem);
#endif
#ifdef XR_USE_GRAPHICS_API_D3D12
FOpenXRRenderBridge* CreateRenderBridge_D3D12(XrInstance InInstance, XrSystemId InSystem);
#endif
#ifdef XR_USE_GRAPHICS_API_OPENGL
FOpenXRRenderBridge* CreateRenderBridge_OpenGL(XrInstance InInstance, XrSystemId InSystem);
#endif
#ifdef XR_USE_GRAPHICS_API_VULKAN
FOpenXRRenderBridge* CreateRenderBridge_Vulkan(XrInstance InInstance, XrSystemId InSystem);

// OpenXRHMD_RenderBridge.cpp

// D3D11的渲染桥接器
class FD3D11RenderBridge : public FOpenXRRenderBridge
{
public:
    FD3D11RenderBridge(XrInstance InInstance, XrSystemId InSystem);
    virtual FXRSwapChainPtr CreateSwapchain(...) override final;

    (...)
};

// D3D12的渲染桥接器
class FD3D12RenderBridge : public FOpenXRRenderBridge
{
public:
    FD3D12RenderBridge(XrInstance InInstance, XrSystemId InSystem);
    virtual FXRSwapChainPtr CreateSwapchain(...) override final

    (...)
};

// OpenGL的渲染桥接器
class FOpenGLRenderBridge : public FOpenXRRenderBridge
{
public:
    FOpenGLRenderBridge(XrInstance InInstance, XrSystemId InSystem);
    virtual FXRSwapChainPtr CreateSwapchain(...) override final

    (...)
};

// Vulkan的渲染桥接器
class FVulkanRenderBridge : public FOpenXRRenderBridge
{
public:
    FVulkanRenderBridge(XrInstance InInstance, XrSystemId InSystem);
    virtual FXRSwapChainPtr CreateSwapchain(...) override final

    (...)
};

由上面可知,OpenXR涉及的类型比较多,主要包含FOpenXRARSystem、FOpenXRHMDPlugin、FOpenXRHMD、FOpenXRRenderBridge等继承树类型。它们各自的继承关系可由以下UML图表达:

classDiagram-v2
IARSystemSupport <|-- FARSystemSupportBase
FARSystemSupportBase <|-- FOpenXRARSystem

class FOpenXRARSystem{
FOpenXRHMD* TrackingSystem;
}

IHeadMountedDisplayModule <|-- IOpenXRHMDPlugin
IOpenXRHMDPlugin <|-- FOpenXRHMDPlugin
class FOpenXRHMDPlugin{
XrInstance Instance;
XrSystemId System;
IOpenXRExtensionPlugin* ExtensionPlugins;
FOpenXRRenderBridge* RenderBridge;
}

IXRTrackingSystem <|-- FXRTrackingSystemBase
FXRTrackingSystemBase <|-- FHeadMountedDisplayBase
IHeadMountedDisplay <|-- FHeadMountedDisplayBase
IStereoRendering <|-- FHeadMountedDisplayBase

FHeadMountedDisplayBase <|-- FOpenXRHMD
FXRRenderTargetManager <|-- FOpenXRHMD
FSceneViewExtensionBase <|-- FOpenXRHMD

class FOpenXRHMD{
XrInstance Instance;
XrSystemId System;
FOpenXRRenderBridge* RenderBridge;
IRendererModule* RendererModule;
}

FRHIResource <|-- FRHICustomPresent
FRHICustomPresent <|-- FXRRenderBridge
FXRRenderBridge <|-- FOpenXRRenderBridge
FOpenXRRenderBridge <|-- FD3D11RenderBridge
FOpenXRRenderBridge <|-- FD3D12RenderBridge
FOpenXRRenderBridge <|-- FOpenGLRenderBridge
FOpenXRRenderBridge <|-- FVulkanRenderBridge

将它们关联起来:

classDiagram-v2
IARSystemSupport <|-- FARSystemSupportBase
FARSystemSupportBase <|-- FOpenXRARSystem

FOpenXRARSystem *-- FOpenXRHMD

IHeadMountedDisplayModule <|-- IOpenXRHMDPlugin
IOpenXRHMDPlugin <|-- FOpenXRHMDPlugin

FOpenXRHMDPlugin ..> FOpenXRARSystem
FOpenXRHMDPlugin --> FOpenXRRenderBridge
FOpenXRHMD --> FOpenXRRenderBridge

IXRTrackingSystem <|-- FXRTrackingSystemBase
FXRTrackingSystemBase <|-- FHeadMountedDisplayBase
IHeadMountedDisplay <|-- FHeadMountedDisplayBase
IStereoRendering <|-- FHeadMountedDisplayBase

FHeadMountedDisplayBase <|-- FOpenXRHMD

FRHIResource <|-- FRHICustomPresent
FRHICustomPresent <|-- FXRRenderBridge
FXRRenderBridge <|-- FOpenXRRenderBridge

那么,以上的重要类型怎么和UE的主循环关联起来呢?答案就在下面:

// UnrealEngine.cpp

bool UEngine::InitializeHMDDevice()
{
    (...)

    // 获取HMD的模块列表.
    FName Type = IHeadMountedDisplayModule::GetModularFeatureName();
    IModularFeatures& ModularFeatures = IModularFeatures::Get();
    TArray<IHeadMountedDisplayModule*> HMDModules = ModularFeatures.GetModularFeatureImplementations<IHeadMountedDisplayModule>(Type);

    (...)

    for (auto HMDModuleIt = HMDModules.CreateIterator(); HMDModuleIt; ++HMDModuleIt)
    {
        IHeadMountedDisplayModule* HMDModule = *HMDModuleIt;

        (...)

        if(HMDModule->IsHMDConnected())
        {
            // 通过XR模块创建追踪系统实例(即IXRTrackingSystem实例,如果是OpenXR,则是FOpenXRHMD), 并将实例保存到UEngine的XRSystem变量中。
            XRSystem = HMDModule->CreateTrackingSystem();

            if (XRSystem.IsValid())
            {
                HMDModuleSelected = HMDModule;
                break;
            }
        }

        (...)
}

以上创建和初始化代码不仅对OpenXR有效,也对其它类型的XR(如FAppleARKitModule、FGoogleARCoreBaseModule、FGoogleVRHMDPlugin、FOculusHMDModule、FSteamVRPlugin等等)有效。

15.3.2.4 Oculus VR

Oculus的XR插件源码是:https://github.com/Oculus-VR/UnrealEngine/tree/4.27。当然,UE 4.27的官方版本已经内置了Oculus插件代码,目录是:Engine\Plugins\Runtime\Oculus\。插件内继承或实现了UE的一些重要的XR类型:

// IOculusHMDModule.h

// 此模块的公共接口。在大多数情况下,此接口仅对该插件中的同级模块公开。
class IOculusHMDModule : public IHeadMountedDisplayModule
{
public:
    static inline IOculusHMDModule& Get();
    static inline bool IsAvailable();

    // 获取HMD的当前方向和位置。如果位置跟踪不可用,DevicePosition将为零向量.
    virtual void GetPose(FRotator& DeviceRotation, FVector& DevicePosition, FVector& NeckPosition, bool bUseOrienationForPlayerCamera = false, bool bUsePositionForPlayerCamera = false, const FVector PositionScale = FVector::ZeroVector) = 0;
    // 报告原始传感器数据。如果HMD不支持任何参数,则将其设置为零。
    virtual void GetRawSensorData(FVector& AngularAcceleration, FVector& LinearAcceleration, FVector& AngularVelocity, FVector& LinearVelocity, float& TimeInSeconds) = 0;

    // 返回用户配置。
    virtual bool GetUserProfile(struct FHmdUserProfile& Profile)=0;
    virtual void SetBaseRotationAndBaseOffsetInMeters(FRotator Rotation, FVector BaseOffsetInMeters, EOrientPositionSelector::Type Options) = 0;
    virtual void GetBaseRotationAndBaseOffsetInMeters(FRotator& OutRotation, FVector& OutBaseOffsetInMeters) = 0;
    virtual void SetBaseRotationAndPositionOffset(FRotator BaseRot, FVector PosOffset, EOrientPositionSelector::Type Options) = 0;
    virtual void GetBaseRotationAndPositionOffset(FRotator& OutRot, FVector& OutPosOffset) = 0;
    virtual class IStereoLayers* GetStereoLayers() = 0;
};

总体上,结构和OpenXR比较类似,本文就不再累述,有兴趣的同学可到插件目录下研读源码。更多可参阅:

15.3.3.1 帧率优化

大部分VR应用都会执行自己的流程来控制VR帧率。因此,需要在虚幻引擎4中禁用多个会影响VR应用的一般项目设置。设置以下步骤,禁用虚幻引擎的一般帧率设置:

  • 在编辑器主菜单中,选择编辑->项目设置,打开项目设置窗口。

  • 在项目设置窗口中,在引擎部分中选择一般设置。

  • 在帧率部分下:

    • 禁用平滑帧率。

    • 禁用使用固定帧率。

    • 将自定义时间步设置为None。

15.3.3.2 体验优化

模拟症是一种在沉浸式体验中影响用户的晕动症。下表介绍的最佳实践能够限制用户在VR中体验到的不适感。

  • 保持帧率: 低帧率可能导致模拟症。尽可能地优化项目,就能改善用户的体验。Oculus Quest 1和2、HTC Vive、Valve Index、PSVR、HoloLens 2、的目标帧率是90,而ARKit、ARCore的目标帧率是60。

  • 用户测试: 让不同的用户进行测试,监控他们在VR应用中体验到的不适感,以避免出现模拟症。

  • 让用户控制摄像机: 电影摄像机和其他使玩家无法控制摄像机移动的设计是沉浸式体验不适感的罪魁祸首。应当尽量避免使用头部摇动和摄像机抖动等摄像机效果,如果用户无法控制它们,就可能产生不适感。

  • FOV必须和设备匹配: FOV值是通过设备的SDK和内部配置设置的,并且与头显和镜头的物理几何体匹配。因此,FOV无法在虚幻引擎中更改,用户也不得修改。如果FOV值经过了更改,那么在你转动头部时,世界场景就会产生扭曲,并引起不适感。

  • 使用较暗的光照和颜色,并避免产生拖尾:在设计VR元素时,你使用的光照与颜色应当比平常更为暗淡。在VR中,强烈鲜明的光照会导致用户更快出现模拟症。使用偏冷的色调和昏暗的光照,就能避免用户产生不适感,还能避免屏幕中的亮色和暗色区域之间产生拖尾。

  • 移动速度不应该变化: 用户一开始就应当是全速移动,而不是逐渐加快至全速。

  • 避免使用会大幅影响用户所见内容的后期处理效果: 避免使用景深和动态模糊等后期处理效果,以免用户产生不适感。

15.3.3.3 其它优化

避免使用以下VR中存在问题的渲染技术:

  • 屏幕空间反射(SSR): 虽然SSR能够在VR中生效,但其产生的反射可能与真实世界中的反射不匹配。除了SSR之外,你还可以使用反射探头,它们的开销较低,也较不容易出现反射匹配的问题。
  • 屏幕空间全局光照: 在HMD中,屏幕空间技巧可能会使两眼显示的内容出现差异。这些差异可能导致用户产生不适感。
  • 光线追踪: VR应用目前使用的光线追踪无法维持必要的分辨率和帧率,难以提供舒适的VR体验。
  • 2D用户界面或广告牌Sprites: 2D用户界面或广告牌Sprite不支持立体渲染,因为它们在立体环境下表现不佳,可以改用3D世界场景中的控件组件。
  • 法线贴图:在VR中观看法线贴图或物体时,会发现它们并没有产生之前的效果,因为法线贴图没有考虑到双目显示或动态视差。因此,在VR设备下观看时,法线贴图通常是扁平的。然而,并不意味着不应该或不需要使用法线贴图,只不过需要更仔细地评估,传输进法线贴图的数据是否可以用几何体表现出来。可以使用视差贴图代替:视差贴图是法线贴图的升级版,它考虑到了法线贴图未能考虑的深度提示。视差贴图着色器可以更好地显示深度信息,让物体看起来拥有更多细节。因为无论你从哪个角度观看,视差贴图总是会自行修正,展示出你的视角下正确的深度信息。视差贴图最适合用于鹅卵石路面,以及带有精妙细节的表面。

UE的其它VR优化:

  • 不使用动态光照和阴影。

  • 不大量使用半透明。

  • 可见批次中的实例。如实例化群组中的一个元素为可见,则整个群组均会被绘制。

  • 为所有内容设置 LOD。

  • 简化材质复杂程度,减少每个物体的材质数量。

  • 烘焙重要性不高的内容。

  • 不使用能包含玩家的大型几何体。

  • 尽量使用预计算的可见体积域。

  • 启用VR实例化立体 / 移动VR多视图。

  • 禁用后处理。由于VR的渲染要求较高,因此需要禁用诸多默认开启的高级后期处理功能,否则项目可能出现严重的性能问题。执行以下步骤完成项目设置。

    • 在关卡中添加一个后期处理(PP)体积域。

    • 选择PP体积域,然后在Post Process Volume部分启用 Unbound 选项,使PP体积域中的设置应用到整个关卡。

    • 打开Post Process VolumeSettings,前往每个部分将启用的PP设置禁用:先点击属性,然后将默认值(通常为 1.0)改为0即可禁用功能。

执行此操作时,无需点击每个部分并将所有属性设为 0。可先行禁用开销较大的功能,如镜头光晕(Lens Flares)、屏幕空间反射(Screen Space reflections)、临时抗锯齿(Temporal AA)、屏幕空间环境遮挡(SSAO)、光晕(Bloom)和其他可能对性能产生影响的功能。

  • 针对平台设置合理的内存桶。使用者可以对拥有不同内存性能的不同平台运行UE4项目的方式进行指定,并添加 内存桶 指定其将使用的选项。要添加此性能,首先需要打开文本编辑程序中的项目 Engine.ini 文件(使用 Android/AndroidEngine.iniIOS/IOSEngine.ini,或任意 PlatformNameEngine.ini 文件以平台为基础进行设置)。为了方便使用,其中已经有一些默认设置,以下是AndroidEngine.ini的示例参数设置:

    [PlatformMemoryBuckets]
    LargestMemoryBucket_MinGB=8
    LargerMemoryBucket_MinGB=6
    DefaultMemoryBucket_MinGB=4
    SmallerMemoryBucket_MinGB=3
     ; for now, we require 3gb
    SmallestMemoryBucket_MinGB=3

    可以在 DeviceProfiles.ini 中指定哪个内存桶与哪个设备设置相关联。例如,要调整纹理流送池使用的内存量,应向DeviceProfiles.ini文件添加以下信息:

    [Mobile DeviceProfile]
    +CVars_Default=r.Streaming.PoolSize=180
    +CVars_Smaller=r.Streaming.PoolSize=150
    +CVars_Smallest=r.Streaming.PoolSize=70
    +CVars_Tiniest=r.Streaming.PoolSize=16

    其中"Mobile"可以替换成要添加设备描述的平台名。使用内存桶还可指定要使用的渲染设置。在下例中,使用 场景设置 的纹理的 TextureLODGroup 已完成设置,UE4检测到使用最小内存桶的设备时将把 MaxLODSize 从1024调整为256,减少自身LOD群组设为"场景"的纹理所需要的内存。

    [Mobile DeviceProfile]
    +TextureLODGroups=(Group=TEXTUREGROUP_World, MaxLODSize=1024, OptionalMaxLODSize=1024, OptionalLODBias=1, MaxLODSize_Smaller=1024, MaxLODSize_Smallest=1024, MaxLODSize_Tiniest=256, LODBias=0, LODBias_Smaller=0, LODBias_Smallest=1, MinMagFilter=aniso, MipFilter=point)
  • 选择合适的线程同步方式。UE支持以下几种线程同步方式:

    • r.GTSyncType 0:游戏线程与渲染线程同步(旧行为,默认)。
    • r.GTSyncType 1:游戏线程与RHI线程同步(相当于采用并行渲染前的UE4)。
    • r.GTSyncType 2:游戏线程与交换链同步,显示+/-以毫秒为单位表示的偏移。为实现此模式同步,引擎通过调用Present()时传入驱动程序的索引跟踪显示的帧。此索引是从平台帧翻转统计数据检索的,它指示每帧翻转的精确时间。引擎用户使用这些值来预测下一帧应于何时翻转,然后基于该时间启动下一个游戏线程帧。

    另外,rhi.SyncSlackMS决定应用到预测的下一次垂直同步时间的偏移。减小该值将缩小输入延迟,但是会缩短引擎管线,更容易出现由卡顿造成的掉帧。相似地,增大该值会延长该引擎管线,赋予游戏更多应对卡顿的弹性,但是会增大输入延迟。一般来说,使用这个新的帧同步系统的游戏应在维持可接受帧率的情况下尽可能缩小rhi.SyncSlackMS。例如,更新率为30 Hz的游戏具有以下CVar设置:

    • rhi.SyncInterval 2
    • r.GTSyncType 2
    • r.OneFrameThreadLag 1
    • r.Vsync 1
    • rhi.SyncSlackMS 0

    它将拥有的最佳输入延迟为约66ms(两个30Hz帧)。如果将rhi.SyncSlackMS增大至10,则最佳输入延迟为约76ms。r.GTSyncType 2也适用于更新率为60Hz的游戏(即,rhi.SyncInterval 设置为1),但是采用此设置的好处不易察觉,由于与30hz相比,帧率为两倍,输入延迟会降低一半。

  • 在渲染线程重新获取HMD的姿态,以减少延迟。

    上:在模拟开始时在本机上查询姿势,并使用该姿势进行渲染,头戴式显示器可能会感受到"迟缓"或缓慢,因为现在在查询设备位置和显示结果帧之间可能会有两帧时间;下:在渲染之前重新查询姿势并使用更新后的姿势来计算渲染的变换,就可以解决这个问题。

  • 其它:开启VSync、开启DynRes、准确使用组合器(Compositor)等等。

更多UE的XR优化可参阅:

在UE4中可通过以下方式获取游戏中的整体数据。

stat unit:可显示整体游戏线程、绘制线程和 GPU 时间,以及整体的帧时。这最适用于收集以下信息:整体总帧时是否处于理想区间、游戏线程时间,但不可用于收集绘制线程和 GPU 时间。

startfpschart / stopfpschart:如果需要了解 90Hz 以上花费的时间百分比,可运行这些命令。它将捕捉并聚合开始和结束之间窗口上的数据,并转存带有桶装帧率信息的文件。注意,游戏有时会报告略低于90Hz,但实际却为90。最好检查80+的桶(bucket),确定在帧率上消耗的实际时间。

stat gpu:与GPU分析工具提供数据相似,玩家可在游戏中观察并监控这些数据,适用于快速检查GPU工作的开销。

如果需要在游戏进程中收集数据(例如用于图表中),实时数据则尤其实用。实时显示可用于分析在控制台变量或精度设置上启用的功能,或立即知晓结果在编辑器中进行优化。数据在代码中被声明为浮点计数器,如: DECLARE_FLOAT_COUNTER_STAT(TEXT("Postprocessing"), Stat_GPU_Postprocessing, STATGROUP_GPU);渲染线程代码块可与 SCOPED_GPU_STAT 宏一同被 instrument,工作原理与 SCOPED_DRAW_EVENT 相似,如: SCOPED_GPU_STAT(RHICmdList, Stat_GPU_Postprocessing);与绘制事件不同,GPU 数据为累积式。可为相同数据添加多个条目,它们将被聚合。为被显示标记的内容应被包含在包罗 [unaccounted] 数据中。如该数据较高,则说明尚有内容未包含在显式数据中,需要添加更多宏进行追踪。

此外,Oculus和SteamVR均有用于了解性能的第三方工具,建议使用这些工具查看实际的帧时和合成器开销,或者借助RenderDoc等第三方调试软件。

Oculus HMD内置的性能分析工具。

15.4 本篇总结

本篇主要阐述了XR的各类渲染技术,以及UE的XR集成的渲染流程和主要算法,使得读者对此模块有着大致的理解,至于更多技术细节和原理,需要读者自己去研读UE源码发掘。推荐几个比较完整、全面、深入的XR课程和书籍:

特别说明

  • 感谢所有参考文献的作者,部分图片来自参考文献和网络,侵删。
  • 本系列文章为笔者原创,只发表在博客园上,欢迎分享本文链接,但未经同意,不允许转载
  • 系列文章,未完待续,完整目录请戳内容纲目
  • 系列文章,未完待续,完整目录请戳内容纲目
  • 系列文章,未完待续,完整目录请戳内容纲目

参考文献