<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 剖析虛幻渲染體系(15)- XR專題

     

     

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


    特別說明

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

     

    參考文獻

    posted @ 2022-06-09 01:26  0向往0  閱讀(522)  評論(2編輯  收藏  舉報
    国产美女a做受大片观看