剖析虛幻渲染體系(15)- XR專題

2022-06-09 09:00:37

 

 

15.1 本篇概述

虛擬現實(VR)因伊萬·薩瑟蘭(Ivan Sutherland)在20世紀60年代的工作而廣受讚譽。在過去的50年裡,虛擬現實技術的普及程度上下波動。特別是,近年來,虛擬現實在虛擬現實及其對應產品擴增實境(AR)方面的投資吸引了許多科技巨頭的注意。例如,2014年,Facebook以20億美元收購了虛擬現實技術公司Oculus,並開始推動虛擬現實進入新時代。如今,蘋果、谷歌、索尼和三星等所有主要廠商都在這一領域投入了大量資金,努力讓虛擬現實變得可存取且價格合理。

15.1.1 本篇內容

本篇主要闡述XR的以下內容:

  • XR概述
  • XR技術
  • XR的引擎整合
  • XR生態

15.1.2 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體驗的型別以及它們設計用於啟用的用例的複雜性而有所不同。

15.1.3 XR綜述

虛擬現實的歷史通常可以追溯到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使用者:

15.1.4 XR生態

VR/AR行業覆蓋了硬體、系統、平臺、開發工具、應用以及消費內容等諸多方面。作為一個還未成熟的產業,VR/AR行業的產業鏈還比較單薄,參與廠商(尤其是內容提供方)比較少,投入力度不是太大。核心內容生產工具面臨較大的研發製作瓶頸,如360°全景拍攝相機,市面上的產品屈指可數。

當前XR涉及了硬體、OS、引擎、裝置製造等等產業,涉及的公司和品牌數不勝數:

隨著近年來,投資圈在XR圈的活躍,相信生態圈會越來越完善,正如2010年前後的智慧移動裝置。

15.1.5 XR應用

當前,由於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技術

15.2.1 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。

15.2.2 光學和成像

所有鏡頭都會引入影象扭曲、色差和其它失真,我們需要在軟體中儘可能地對其進行校正!通過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毫秒。

15.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毫秒越近越好。如果延遲不可避免,那麼變化越大,就越不舒服,目標應該是儘可能降低和減少可變延遲。

15.2.4 渲染

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級別的法線貼圖粗糙度相加/求和,因為有各向異性光澤貼圖,所以儲存生成的法線貼圖粗糙度是免費的。

左:各向同性光澤度;右:各向異性光澤度。

15.2.5 XR優化

單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優化

15.2.6 其它

早期的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

15.3.1 UE XR概述

早在UE3時代,就已經通過節點圖支援了VR的渲染,其中渲染管線的不同元件可以在多個設定中重新排列、修改和重新連線。根據所支援的節點型別,有時可以以產生特定VR技術效果的方式設定節點。下圖顯示了一個範例,它描述了虛幻引擎的材質編輯介面,該介面被設定為渲染紅色青色立體影象作為後處理效果。

使用UE3的「材質編輯器」(Material Editor)設定虛幻引擎以支援紅青色立體感,可以以這種方式支援其它立體編碼,例如通過隔行掃描用於偏振立體顯示的影象。

以下是2013前後的遊戲引擎對VR的支援情況表:

時至今日,UE4.27及之後的版本已經支援AR、VR、MR等技術,支援Google、Apple、微軟、Maigic Leap、Oculus、SteamVR、三星等公司及其旗下的眾多XR平臺,當然也包括OpenXR等標準介面。

15.3.2 UE XR原始碼分析

剖析虛幻渲染體系(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 UE VR優化

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優化可參閱:

15.3.4 UE VR效能檢測

在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課程和書籍:


特別說明

  • 感謝所有參考文獻的作者,部分圖片來自參考文獻和網路,侵刪。
  • 本系列文章為筆者原創,只發表在部落格園上,歡迎分享本文連結,但未經同意,不允許轉載
  • 系列文章,未完待續,完整目錄請戳內容綱目
  • 系列文章,未完待續,完整目錄請戳內容綱目
  • 系列文章,未完待續,完整目錄請戳內容綱目

 

參考文獻