元宇宙時代的來臨對實時3D引擎提出了諸多要求,Unity作為游戲行業(yè)應(yīng)用最廣泛的3D實時內(nèi)容創(chuàng)作引擎,為應(yīng)對這些新挑戰(zhàn),提出了Unity云原生分布式運行時的解決方案。LiveVideoStack 2023上海站邀請到Unity中國的解決方案工程師舒潤萱,和大家分享該方案的實踐案例、面臨的問題、解決方式,并介紹了Unity目前對其他方案的構(gòu)想。
文/舒潤萱
大家好,我叫舒潤萱,現(xiàn)在在Unity中國擔(dān)任解決方案工程師,主要負責(zé)開發(fā)的項目是Unity云原生分布式運行時。
首先介紹一下Unity。Unity是游戲行業(yè)應(yīng)用最廣泛的3D實時內(nèi)容創(chuàng)作引擎。截止2021年第4季度,70%以上移動平臺的游戲是使用Unity開發(fā)的。
但Unity不止是一個游戲引擎,Unity的業(yè)務(wù)目前涉及到汽車行業(yè)、建筑行業(yè)、航空航天行業(yè)、能源行業(yè)等等各行各業(yè)。
Unity的業(yè)務(wù)在全球都有開展,在18個國家有54個辦公室。在中國,在上海、北京、廣州都有辦公室,在臨港也開了一間新的辦公室。
Unity覆蓋的平臺是最廣泛的,它支持超過20個主流平臺,率先支持了Apple新發(fā)布的vision OS。
我今天的分享將從這六個方面進行。
-01-
元宇宙時代的挑戰(zhàn)
首先,實時3D引擎在元宇宙時代會遇到什么挑戰(zhàn)?
Unity認為元宇宙會是下一代的互聯(lián)網(wǎng),它將是實時的3D的、交互的、社交性的,并且是持久的。
元宇宙會是一個規(guī)模龐大的虛擬世界,這個虛擬世界里有很多參與者,它將會是一個大量用戶實時交互的場景。同時,元宇宙它必須是一個持續(xù)穩(wěn)定的虛擬世界。
參與者在這個虛擬世界里對它進行的改變將會隨著時間保留下來,并且它是穩(wěn)定的狀態(tài)。這就對實時3D交互造成了很多挑戰(zhàn)。
首先,因為這個虛擬世界將會是一個大規(guī)模的高清世界,里面將會有數(shù)量龐大的動態(tài)元素、靜態(tài)元素,所以它對實時3D引擎提出了渲染的挑戰(zhàn)。
其次,它伴隨著大量的網(wǎng)絡(luò)傳輸,對引擎的可擴展性和可伸縮性提出了很高要求,所以對運行平臺來說也是一個不小的挑戰(zhàn)。
最后,因為這個虛擬世界會有超大規(guī)模的物理仿真,用戶將會在其中進行大量的實時交互,所以這對運算資源也是一個巨大的挑戰(zhàn)。
-02-
Unity分布式運行時
為了應(yīng)對這些挑戰(zhàn),Unity提出了分布式運行時的解決方案。
這個方案由兩部分組成,第一個部分是Unity云原生的分布式渲染,第二個部分是Unity云原生分布式計算。
首先介紹分布式渲染。要做一個多人聯(lián)網(wǎng)的體驗,可以從最簡單的Server-Client架構(gòu)入手。如圖所示,在這個架構(gòu)中有一個中心化的Server,它可以服務(wù)于多個Client。這張圖上畫了兩個Client,這是最簡單的架構(gòu)。
為了對應(yīng)大規(guī)模的渲染壓力,Unity把用戶的Client端拆成了Merger和Renderer。在這里面,一個Merger對應(yīng)多個Renderer,這些Renderer將會負責(zé)虛擬世界的渲染。我們把一幀的渲染任務(wù)拆分成很多個子任務(wù),分別交給這些Render進行。Merger負責(zé)把Renderer渲染的畫面組合成最終畫面,之后通過WebRTC推流的方案推給用戶的客戶端。要注意,這里除了用戶的客戶端以外,所有的環(huán)境都是運行在云上的。
圖示為一個最簡單的屏幕空間拆分,即把一個完整的畫面在屏幕空間上拆成幾個部分。除此之外還有其他拆分方式,例如通過時間拆分:假設(shè)有4個Renderer,第一個Renderer渲染第一幀,第二個Renderer渲染第二幀,第三個Renderer渲染第三幀……以此類推,再把它們組合成一個完整的序列。
Unity現(xiàn)在也在實驗一個新的拆分方式:在Merger上渲染一個比較高清的近景,再把遠景拆分到幾個Renderer上。Renderer的幾個畫面可以組成一個天空盒推到Merger上,這樣Merger就可以同時擁有細節(jié)比較豐富的近景和遠景。Unity的架構(gòu)允許開發(fā)者根據(jù)自己的業(yè)務(wù)需求定義自己畫面的拆分方式。
回到這個架構(gòu)圖,想象在一個大型的虛擬世界里,有成千上萬用戶連入運行時,通過分布式渲染,把原本一個進程服務(wù)對應(yīng)一個用戶的場景,拆分成好幾個進程服務(wù)于一個用戶,因此這個場景就會對Server提出巨大的挑戰(zhàn)。為了應(yīng)對這個挑戰(zhàn),Unity提出了分布式計算。
這種分布式計算把Server拆分成了Server和Remote,用 Remote分擔(dān)Server的一些運算壓力。
簡要概述一下Unity是怎樣把計算任務(wù)分配給Remote的。Unity游戲的業(yè)務(wù)流程可以簡單拆分成數(shù)據(jù)和數(shù)據(jù)處理器,數(shù)據(jù)叫做Component,數(shù)據(jù)處理器實際上就是業(yè)務(wù)邏輯,代碼叫System。每個System有自己感興趣的數(shù)據(jù),它會讀取這個感興趣的數(shù)據(jù),通過邏輯進行數(shù)據(jù)更新。
Unity把這個System跑在遠程的機器上,而不是本地,之后把它感興趣的數(shù)據(jù)通過網(wǎng)絡(luò)發(fā)送給遠程的機器,它就可以像本地處理一樣處理這個數(shù)據(jù),再把更新的數(shù)據(jù)通過網(wǎng)絡(luò)發(fā)回給Server。
視頻編解碼與實時渲染
下面進入今天的正題,視頻編解碼與實時渲染。
為什么要在分布式渲染中引入視頻編解碼?
可以看這個簡化的分布式渲染架構(gòu)圖。Merger和Renderer之間的通信是通過網(wǎng)絡(luò)TCP傳輸?shù)?,即圖像是通過TCP傳輸?shù)摹?/p>
這個做法有兩個問題:
一是會遇到網(wǎng)絡(luò)帶寬瓶頸,在實時3D交互的場景下,一般至少要Target到1080p60幀。而如果傳的是8 bits RGBA的圖像格式,需要的帶寬是4Gbps,遠遠超出了常見的千兆帶寬1 Gbps,很多機房沒有辦法接受。即使對Raw Data進行YUV420的簡單壓縮,它也要占用約1.6 Gbps的帶寬,超過了千兆。
其次是性能問題。因為Unity的畫面是從GPU渲染出來的,為了通過網(wǎng)絡(luò)傳輸這個畫面,要先把GPU顯存上的圖像數(shù)據(jù)先回傳到CPU端。這個回讀就會導(dǎo)致需要暫停渲染的流水線,因為目前Unity優(yōu)化較好的實時3D引擎的渲染都是流水線形式的,即CPU產(chǎn)生場景數(shù)據(jù),再把這個場景數(shù)據(jù)交給GPU渲染,同時CPU可以進行下一幀的計算。但是如果數(shù)據(jù)是反過來從GPU回到CPU,意味著需要把流水線暫停,包括GPU、CPU上的所有任務(wù)都要等待回讀完成之后才能繼續(xù)工作。
這里截取了一張Unity的回讀指令,它消耗的時間是7.22毫秒,在實時交互的場景下,希望做到的是60幀的體驗,即16毫秒,因此用7.22毫秒的時間把這張圖片讀回來是無法接受的。
為了解決這兩個問題,Unity為分布式渲染的方案開發(fā)了Unity Native Render Plugin。Unity的目標(biāo)運行環(huán)境是云端,Linux和Vulkan的圖形API。云端一般配備的是NVIDIA GPU,希望采用NVIDIA GPU進行硬件的視頻編解碼。在這個方案下面,Unity還是負責(zé)虛擬世界的渲染和邏輯。FFmpeg負責(zé)視頻的編解碼以及把Unity的圖像在顯存內(nèi)部拷貝給NVIDIA的Video Codec SDK,Unity的插件就是連接這兩個部分以實現(xiàn)目標(biāo)。
這幾個流程圖展示了Unity一定要硬件編解碼的原因。橙色的框為顯存上的數(shù)據(jù),藍色的框為CPU端內(nèi)存上的數(shù)據(jù)。
最左邊的流程圖是最簡單的。如果要傳輸一個Raw Data的方案,在GPU上把圖像渲染完成之后,要通過回讀把它讀到CPU端,這是非常耗時的操作。在讀完這個圖像之后,把它通過TCP發(fā)送給Merger,占用帶寬非常大,這是不可接受的。
如果引入的是一個軟件編解碼的場景,需要圖像的數(shù)據(jù)在CPU端存在。所以回讀還是無法避免,操作仍然十分耗時。在軟件編解碼時進行的編碼和解碼操作,同樣非常耗時。雖然這對視頻來說沒問題,但是對于60幀的實時場景是不太能接受的。但是這個方案的帶寬其實優(yōu)化了很多,因為視頻碼率至少比4 Gbps、1.5 Gbps好很多。這是軟件面板的情況,它最多只是優(yōu)化了帶寬。
最后是硬件編解碼。硬件編解碼是在渲染完成之后,通過顯存間的拷貝,把渲染的結(jié)果拷貝到Media的CUDA端,然后做硬件編碼和硬件解碼。雖然是一次拷貝,但是因為是在一張顯卡的顯存上,所以拷貝過程非??臁S布幋a完成之后,視頻幀數(shù)據(jù)會自動回到CPU端。雖然其本質(zhì)上也是一次回讀,但是它和流水線無關(guān),相當(dāng)于是在另一條線上完成的,所以這個回讀消耗的只有從PCIE到CPU到內(nèi)存的帶寬的速度,回讀的不是一個完整的圖像,而是壓縮好的視頻幀,它的數(shù)據(jù)量也大大減小,所以回讀非???。之后通過TCP發(fā)送視頻幀,帶寬非常小,在Merger端解碼上傳的Buffer也比原來小,解碼也很快。最后把解碼完成后的Image,Copy 到Vulkan Texture,這也是在同一塊GPU內(nèi)顯存里的操作。
可以看到,硬件編解碼的流程優(yōu)化了很多,每一步都能得到很高的提升。
介紹一下Unity是怎樣配置FFmpeg的CodecContext的。Unity使用了兩個CodecContext,Video Context是真正用來做視頻編解碼的Context,它是一個TYPE CUDA的Hwdevice。第二個CodecContext用的是Vulkan的Context,這個Context的作用是
和Unity的Vulkan Context進行交互,所以在初始化時不會使用到它默認的API,而是從Unity的Vulkan Instance里面取出了所有必要的信息,把它交給FFmpeg的Vulkan Context,最后通過這種方式,就可以讓FFmpeg的環(huán)境和Unity的環(huán)境存在于同一個Vulkan的運行環(huán)境中。
Video Context做初始化時,沒有用到它默認的Hwdevice Context create的API,用到的是create derived的API。這個API需要傳入另一個Hwdevice Context,它能保證做初始化時和另一個Hwdevice Context使用的是同一個物理GPU,這樣才能真正地做顯存間拷貝。這里選取的邊界碼的格式是H.264,8 bit,NV 12。 為什么要選用這個格式呢?其實是受硬件限制,因為目前NVIDIA硬件不支持10 bit的H.264的解碼。H.265則耗時較大,不太滿足60幀的需求,所以不得不選取這個格式。同時Unity是Zero Latency的配置,GOP的Size是0,保證視頻每一幀都是Intra Frame。這有兩個好處,一是它的延遲非常低。其次,因為分布式渲染在管理的時候可能會隨機丟棄幀,如果有預(yù)測幀則不好丟棄,在全是I幀的情況下可以隨機丟棄。
-05-
畫質(zhì)與性能優(yōu)化
接下介紹Unity在畫質(zhì)方面和性能方面對插件進行的一些優(yōu)化。
首先是Tone-Mapping。Unity之所以這樣做是因為遇到了Color-Banding問題,這個問題由兩個因素導(dǎo)致:
首先,傳輸?shù)膱D像不是一個普通的SDR圖像,而是HDR圖像;
其次,選取的格式只有8 bit,它導(dǎo)致了顏色精度和范圍都是受限的。
為了理解為什么Unity傳輸?shù)氖荋DR圖像,我給大家簡單介紹一下渲染里面的一幀是怎么生成的。圖示為簡化的渲染過程,真正的渲染過程要比這復(fù)雜很多。
渲染過程可以簡單分為三部分:
第一個部分叫Pre-Pass,它的作用是生成后續(xù)渲染階段所需要的Buffer,在這一階段里會預(yù)先生成Depth,即預(yù)先生成深度緩沖。深度緩沖預(yù)先生成最主要的好處就是可以減少Over-Draw,很多被遮擋的物體直接被剔除掉,后續(xù)步驟中就可以不用畫。
第二步是渲染中最主要的一個步驟,我把它叫做Lighting Pass,主要的作用是計算光照,生成物體的最終顏色。這種Pass的實現(xiàn)方式有很多種,例如前向渲染、延遲渲染等。無論是使用哪種實現(xiàn)方式,它的最終目的都是生成一張Color Buffer,即顏色數(shù)據(jù)。
最后一個步驟一般成為后處理Post-Processing。這個步驟的主要作用是對輸出的Color Buffer進行圖像處理。經(jīng)過Post-Processing產(chǎn)生的圖像,顏色比單純的Color Buffer自然很多。Post-Processing里面的效果大部分都是屏幕空間效果,例如要做一個輝光的效果,畫面中有一盞燈非常亮,在它的邊緣會有一些柔和的光效溢出,這個效果基本上就是通過Post-Processing實現(xiàn)的。
屏幕空間效果會有什么問題?在分布式渲染中,渲染任務(wù)是被拆分開的,所以真正做渲染的Render的機器,實際上是沒有全屏信息的,它沒有辦法做后處理。
對此Unity的解決方案是將后處理交由Merger進行。Renderer渲染到Color Buffer生成之后,就把Color Buffer傳給Merger,Merger先把這個Color Buffer合起來,再總體做一次后處理。
那么,為什么這一過程中傳的Colour Buffer是HDR的?因為在渲染中,光照模型一般都是物理真實的,所以為了之后做后處理,Colour Buffer本身是HDR的。
如圖所示,這是把Black Point和White Point分成設(shè)置成0和1的效果,但是實際上這張圖最高的值可以到90以上。如果是室外的場景,最高值可以到一萬多,所以這張圖的動態(tài)范圍是非常高的。
同時,因為傳的是動態(tài)很高的HDR圖像,加上使用的是8bit的編碼,所以必須找到一個方法把HDR的Corlor Buffer映射為SDR的Buffer。對于這一映射也有一些要求,其一,它需要可逆;其二,它需要保留更大的表示范圍,并且盡量減少精度損失。
Unity在這方面采用的是AMD提出的Fast Reversible Tone-Mapper,它有許多好處: 首先它是Unity SRP原生支持的,是Unity的可編程的渲染管線;其次它是可逆的,如圖是它的公式;再次,它保留的數(shù)字范圍更大,精度損失更小。如圖上這一圖像,y=x可以理解為原始顏色,經(jīng)過Tone-Mapper處理后,它的取值范圍永遠在0~1之間,同時當(dāng)它的值越小時,斜率越大,代表它能表示的數(shù)字越多,可保留的精度越高。有時在渲染中會遇到顏色值非常高的情況,但這種情況少之又少,更多的還是保留在0~1的范圍區(qū)間內(nèi)。因此Tone-Mapper能夠幫助我們在0~1這個區(qū)間范圍內(nèi)保留更多的精度;最后它非常快,在AMD的GCN架構(gòu)的顯卡下,MAX RGB會被編譯為一個指令,整個運算中只有3個指令,Max、加法和除法,所以它是非??斓摹?/p>
如圖,可以對比使用Tone-Mapping前后的效果。最左邊是未經(jīng)任何處理的原始圖像;中間是不使用任何Tone-Mapping,經(jīng)過8 bit的編解碼、網(wǎng)絡(luò)傳輸,再解碼回來的情況;最右邊是應(yīng)用了Fast Reversible Tone-Mapping的情況??梢钥吹嚼锩娴谋尘坝泻芏嗉毠?jié)紋理,代表其中高頻的信息比較多。在高頻信息較多的場景下,應(yīng)用了Fast Reversible Tone-Mapping之后的效果和原始圖像的效果對比,已經(jīng)看不出什么區(qū)別了。
但是如果場景里低頻的信息較多,例如漸變較多,即使運用了Tone-Mapping,也沒有辦法完全解決這個問題??梢钥吹?,原始圖像上的漸變非常柔和,在無Tone-Mapping的情況下,色帶肉眼可見。但是即使引入了Fast Reversible Tone-Mapping,也只能減緩色帶問題,比起原始圖像而言還是差了很多,目前沒有更好的辦法解決這一問題。
Unity對插件進行性能優(yōu)化的另一個方式是Vulkan同步。因為涉及GPU內(nèi)部的顯存拷貝,而且它是Vulkan拷貝到CUDA的操作,所以它是一個GPU-GPU的異步操作。異步操作要求開發(fā)人員對于操作做好同步,即在編碼時,要保證Unity渲染完成之后才進行編碼,解碼時要保證解碼完成之后,才能把這張圖像Copy給Unity,讓Unity把它顯示在屏幕上。
Unity Vulkan Native Plugin Interface提供了一種同步方式。這里主要關(guān)注框出的兩個Flag:
上面的Flush Command Buffer,指在Unity調(diào)用自定義渲染事件時,Unity會先把它已經(jīng)錄制好的渲染指令提交到GPU上,這時GPU就可以開始執(zhí)行這些指令了。
下面的Sync Worker Threads,指在Unity調(diào)用自己定義的Plugin Render Event時,會等待GPU上所有的工作全部完成之后才會調(diào)用。
這兩個方式組合起來確實能滿足需求,確實可以做到同步,但是這種方式也打斷了渲染流水線。使用這種同步方式,在調(diào)用Plugin Event之前,所有程序要全部停下,等待GPU完成操作。因此這個方式雖然能實現(xiàn)同步,但非常耗時。
如圖展示的就是在這個同步方式下的情況。在簡單的場景下,它耗時5.6毫秒。所以這種方式雖然能同步,但是性能非常差。
Unity只提供了上述的同步方式,因此我們只能轉(zhuǎn)向Vulkan自帶的同步原語。Timeline Semaphore是Vulkan在1.2版本的SDK里提出的新型的Semaphore,它非常靈活,而且是FFmpeg原生支持的。這里是FFmpeg的Vulkan Context的Frame,它通過Timeline Semaphore同步。在FFmpeg里,它主要被用于GPU-CPU同步,但它也可以用于GPU-GPU同步。
Unity提供的同步方式只有上述兩個Flag,它無法直接使用底層的同步原語,但是它允許我們Hook Vulkan的任何一個API,即在 Hook之后,Unity在調(diào)用Vulkan的API時,它其實調(diào)用的是我們自己定義的Hook的版本,因此我們使用了Vulkan Hook介入Unity的渲染,在Unity把一幀的渲染提交給GPU之前,通過Hook提交的API,把FFmpeg的Semaphore 塞到提交里面,就可以保證在渲染完成之后會通知Timeline Semaphore,F(xiàn)Fmpeg會等Semaphore被通知之后再執(zhí)行。通過這種方式,這個同步也能達成目的,而且不會打斷渲染流水線。
如圖所示,上面為5.6毫秒耗時的情況,下面則完全把這一耗時消除了。因為在這個情況下,不需要在調(diào)用Render Event之前就提交渲染指令,而是在GPU上通過Timeline Semaphore和FFmpeg的Command進行同步,因此把這一部分完全省去了。在這個簡單場景中大概有5.6毫秒左右的提升,經(jīng)過測試,在復(fù)雜場景中則會有10~20毫秒不等的提升。
另一個性能優(yōu)化方案是多重緩沖,這是渲染中非常常用的技巧。在渲染中,常常會用多重緩沖來減少畫面的卡頓、撕裂等情況,三重緩沖還能夠提高幀率的穩(wěn)定性、提高渲染性能。
多重緩沖的引入會把渲染變成Single Producer, Single Consumer的流水線模型,即渲染流水線。正是因為有多重緩沖,才能形成流水線,讓GPU往一個緩沖中寫入時,CPU可以開始準備下一個緩沖,GPU可以同時往另一緩沖區(qū)寫入下一幀數(shù)據(jù)。
在編解碼的插件中,Unity也引入了多重緩沖來提高性能,使用硬件編解碼代替圖像上屏操作。渲染的圖像沒有顯示在屏幕上,而是通過網(wǎng)絡(luò)發(fā)走,這是通過多重緩沖的方式實現(xiàn)的,和渲染有同樣的效果。在引入多重緩沖后,Unity的渲染和編解碼器會分別作為Producer和Consumer進行渲染和編解碼的流水線。
-06-
總結(jié)與未來展望
在分布式渲染的解決方案中會遇到網(wǎng)絡(luò)帶寬和性能問題。
首先,通過引入視頻編解碼可以解決了網(wǎng)絡(luò)帶寬的問題,采取硬件編解碼避免GPU-CPU的回讀,避免打斷渲染的流水線。為了實現(xiàn)這個目標(biāo),Unity開發(fā)了Unity Native Rendering Plugin來對接Unity和FFmpeg底層的Vulkan和NVIDIA Codec 的SDK。
因為選取的編解碼格式,方案中還遇到了色帶問題,因此在方案中我們引入Tone-Mapping優(yōu)化畫質(zhì),通過FFmpeg自帶的Timeline Semaphore,把Unity的渲染指令和FFmpeg的拷貝指令和編解碼指令進行同步,保證編解碼結(jié)果正確。
最后,Unity通過引入多重緩沖提升性能,減少幀率不穩(wěn)的情況。
目前Unity還在探索一些其他的方案。
首先,Unity希望嘗試Vulkan自己推出的Vulkan Vider Extensions。它在2023年1月左右才真正進入Vulkan的SDK,成為一個正式的功能。這個Extension非常新,所以到目前為止Unity還沒有機會進行嘗試,但一直在關(guān)注。如果使用這一Extension,就可以完全避免前文所述得到顯存間拷貝、同步等問題。因為這一應(yīng)用程序沒有引入別的GPU端的運行環(huán)境,完全在Vulkan內(nèi)部運行,因此我們不需要拷貝,直接使用Unity的結(jié)果即可。
其次,Unity在對接其他GPU的硬件廠商,嘗試其他硬件編碼。Unity目前正在和一些國產(chǎn)的GPU廠商對接,他們表示他們的硬件編解碼能力會有所提升,支持的格式不再限制于8 bits了。
最后,Unity還希望嘗試一些要使用GPUDirect RDMA、CXL共享內(nèi)存等特殊硬件的方案。GPUDirect RDMA允許直接把GPU顯存里的東西直接通過網(wǎng)絡(luò)發(fā)走,能夠減少回讀。CXL共享內(nèi)存,顧名思義是個共享內(nèi)存,相當(dāng)于很多臺機器共享一個遠程的內(nèi)存池,因此它的帶寬和延遲都是內(nèi)存級別。這一方案至少允許我們在分布式渲染的環(huán)境下不進行視頻編解碼,可以使用Raw Data的方式,把Raw Data存在遠程內(nèi)存中。
編輯:黃飛
評論
查看更多