
RHI作為Unreal的一個(gè)跨平臺(tái)渲染封裝, 存在于上層顯示和下層圖形api之間, 即一個(gè)中間接口層。本篇內(nèi)容將通過(guò)draw call的執(zhí)行過(guò)程,認(rèn)識(shí)到Unreal的封裝方法,以及shader與Fshader的具體應(yīng)用。
一、認(rèn)識(shí)draw call的執(zhí)行過(guò)程
1.1
復(fù)習(xí)渲染管線
首先一起來(lái)復(fù)習(xí)一下渲染管線的知識(shí)。如圖1.1.1,是GPU的渲染管線執(zhí)行流程。
圖1.1.1 渲染管線執(zhí)行流程
一個(gè)draw call的產(chǎn)生,由以上的流程所生成的。這是一個(gè)通用的排布, 我們也可以根據(jù)所需進(jìn)行局部調(diào)整。
在這個(gè)管線執(zhí)行過(guò)程前, 即在cpu給gpu發(fā)送指令前,除了具體的數(shù)據(jù)外,傳入的參數(shù)的數(shù)據(jù)格式,也非常重要, 下面會(huì)提及。
1.2
以d3d的角度理解管線裝配過(guò)程
我們以龍書(shū)里的d3d代碼段為案例,去理解管線裝配的過(guò)程。
d3d提供一個(gè)pipelineState,用來(lái)創(chuàng)建管線的狀態(tài)配置。如果要執(zhí)行不同的shader代碼,就要用不同的配置,需要?jiǎng)?chuàng)建不同的pipelineState。即告訴api,我們要用哪個(gè)配置項(xiàng),執(zhí)行哪個(gè)shader。
圖1.2.1 管線的狀態(tài)設(shè)置
我們可以看到,是先申請(qǐng)內(nèi)存,然后再去填充各種狀態(tài)。其中.VS和.PS就是把編譯好的shader代碼放進(jìn)去,這里通過(guò)指針和size給到對(duì)應(yīng)的指定。
而pRootSignature就是規(guī)定好代碼的輸入?yún)?shù)數(shù)據(jù)格式,如下圖1.2.2所示。
圖1.2.2 pRootSignature的輸入?yún)?shù)數(shù)據(jù)格式
構(gòu)造完成之后, 就用CD3DX12_ROOT_SIGNATURE_DESC打包成根簽名描述, 再用它去創(chuàng)建一個(gè)真正的根簽名,即上面的mRootSignature。
東西準(zhǔn)備完了那就要draw了。從圖1.2.3中可以看到,之前創(chuàng)建的mOpaquePSO和 mRootSignature就被放進(jìn)去了。
圖1.2.3 渲染執(zhí)行
渲染管線狀態(tài)和根簽名傳完以后,那么就可以傳入真正的參數(shù)了,本質(zhì)上就是拿虛擬地址,給到顯存映射好。
圖1.2.4 渲染內(nèi)容設(shè)置
另外,IASetVertexBuffers傳入頂點(diǎn)屬性,SetGraphicsRootDescriptorTable傳入貼圖等實(shí)際內(nèi)容填充,都在這里執(zhí)行。在搞定所有內(nèi)容之后,只需要再執(zhí)行 DrawIndexedInstanced就可以讓gpu執(zhí)行了。
圖1.2.5 渲染內(nèi)容填充
二、Unreal的封裝方法
接下來(lái)我們回歸本篇內(nèi)容的核心——在Unreal中要怎么去封裝上述D3D以及其他API的渲染過(guò)程 (限于篇幅這里只提及部分內(nèi)容封裝)
2.1
資源封裝
我們先從資源開(kāi)始,所有參數(shù)的設(shè)置狀態(tài), 本質(zhì)都是一段內(nèi)存或者顯存, 就是d3d的resource, 或者OpenGL的無(wú)符號(hào)整數(shù)resource id。
Unreal提供了一個(gè)FRHIResource , 實(shí)際內(nèi)容可以是uniform buffer,也可以是texture,類(lèi)型可以通過(guò)它的ERHIResourceType 枚舉變量查看到, 如圖2.1.1。
圖2.1.1 資源類(lèi)型
這個(gè)FRHIResource基類(lèi)里沒(méi)定義太多的操作,只有有AddRef,Release和原子操作。
我們往子類(lèi)看,來(lái)到FRHIUniformBuffer,這里的封裝是出于一個(gè)與平臺(tái)無(wú)關(guān)的的狀態(tài),只是定義了對(duì)外的接口。接下來(lái)我們就來(lái)具體講解它的使用方法。
圖2.1.3 FRHIUniformBuffer
接下來(lái)我們?cè)僮宇?lèi)看, 來(lái)到FD3D12UniformBuffer。
圖2.1.4 FD3D12UniformBuffer
來(lái)到熟悉的FD3D12ResourceLocation,我們進(jìn)去看看。
圖2.1.5 FD3D12ResourceLocation里的內(nèi)容
可以看到有FD3D12Resource變量。
圖2.1.6 FD3D12Resource
最終找到了d3d12里真正存儲(chǔ)resource的ID3D12Resource指針。這個(gè)location里面有一個(gè)成員變量,又是一個(gè)FD3D12Resource。而這個(gè)FD3D12Resource里面,就是真正D3D12的指針。當(dāng)然,下面還有一些別的被它封裝了起來(lái),比如GPUVirtualAddress。
所以這就是一套簡(jiǎn)單的繼承體系,我們想用什么接口,就在RHI層面定義一個(gè)接口,在子類(lèi)里去實(shí)現(xiàn)它。
接口就是通用的, 比如Create、Release一段資源,或者Upload一段內(nèi)存進(jìn)去。
同理OpenGL那邊也就會(huì)有一個(gè)FOpenGLUniformBuffer。
圖2.1.7 FOpenGLUniformBuffer
UE RHI只做一些命名和通用接口封裝, 實(shí)現(xiàn)都是在不同的子類(lèi), 編譯到哪種平臺(tái)就用對(duì)應(yīng)的上層邏輯代碼, 只需要關(guān)注操作的是RHI層的東西就可以, 直接調(diào)用已經(jīng)封裝好的接口就行。
接下來(lái)我們以Unreal里最常用的UTexture2D舉個(gè)例子:
圖2.1.8 UTexture2D
比如讓它執(zhí)行UpdateTextureRegions, 它實(shí)際上就是去調(diào)RHI的函數(shù)。
圖2.1.9 UpdateTextureRegions
圖2.1.10 執(zhí)行更新
2.2
執(zhí)行封裝
這里就不得不提到這里出現(xiàn)的FDynamicRHI了。FDynamicRHI是RHI的動(dòng)態(tài)實(shí)現(xiàn)部分,而RHI是更底層的靜態(tài)接口。這個(gè)類(lèi)里的方法超多,比如常見(jiàn)的圖形渲染操作,創(chuàng)建紋理、狀態(tài)、更新資源、設(shè)置Fence、更新貼圖等一大半操作,都在此類(lèi)中。但都沒(méi)實(shí)現(xiàn)。OpenGL、D3D會(huì)去繼承它、實(shí)現(xiàn)它。所以不要在上層寫(xiě)api的實(shí)際調(diào)用內(nèi)容,否則代碼無(wú)法跨平臺(tái)。
這里面都是些純虛函數(shù),同樣下面有D3D和OpenGL的各種各樣的子類(lèi)。
圖2.2.1 FDynamicRHI
三、在Unreal中封裝
3.1
shader的封裝
FRHI shader是繼承FRHIResource, 思路也跟之前一樣。其子類(lèi)D3D和OpenGL會(huì)有具體且復(fù)雜的實(shí)現(xiàn)。
圖3.1.1 FRHI shader
成員Frequency枚舉,用于定義該Shader是哪個(gè)類(lèi)型。
圖3.1.2 成員Frequency枚舉
以FRHIVertexShader為例我們來(lái)看一下, 先直接看d3d怎么做的吧。
圖3.1.3 FRHIVertexShader
這里繼承兩個(gè)類(lèi), 后面的FD3DShaderData才是主要內(nèi)容。
圖3.1.4 FD3DShaderData
比較重要的就是code了, 內(nèi)部將其轉(zhuǎn)換成D3D的bytecode。
根據(jù)代碼去創(chuàng)建一個(gè)shader的話, 就要用到之前提到的FDynamicRHI定義的接口,比如OpenGL的實(shí)現(xiàn)如下。
圖3.1.5 創(chuàng)建shader
進(jìn)入后,先會(huì)解析一下Code,因?yàn)閁nreal這里的code不是純代碼,還有帶有參數(shù),所以會(huì)先用一個(gè)Reader解析。
圖3.1.6 獲取Optional數(shù)據(jù)大小
圖3.1.7 獲取實(shí)際Code大小
可以看到Code前半段是代碼,后半段是Optional的數(shù)據(jù)。代碼長(zhǎng)度=總長(zhǎng)-OptionalData長(zhǎng)度。
解析完代碼以后, 會(huì)進(jìn)行一個(gè)glsl的轉(zhuǎn)換, 因?yàn)镺penGL在各個(gè)平臺(tái)也有適配差異, 是需要做一些適配變化的。
圖3.1.8 glsl轉(zhuǎn)換
最終傳入代碼, 并進(jìn)行編譯操作。
圖3.1.9 輸入并編譯代碼
d3d同理, 這里就不做闡述了。
3.2
FShader簡(jiǎn)介
FShader是上層的一個(gè)類(lèi),F(xiàn)RHIShader相對(duì)于自己那套繼承體系是上層,但相對(duì)于FShader則是底層。有RHI的都是相對(duì)的底層或者中間層。
圖3.2.1 shader創(chuàng)建
FGlobalShader可能大家比較熟悉,自己要寫(xiě)了一個(gè)hlsl的shader,那通常繼承FGlobalShader, 而 FGlobalShader是繼承自FShader。
圖3.2.2 Unreal官方hlsl轉(zhuǎn)glsl shader pipeline的示意圖
所以我們寫(xiě)的usf并不是最終的代碼,還會(huì)繼續(xù)進(jìn)行轉(zhuǎn)換。轉(zhuǎn)換過(guò)程中,就會(huì)去檢測(cè)編譯選項(xiàng),一個(gè)usf中有不同的各種各樣不同的編譯選項(xiàng),即排列組合permutation。根據(jù)不同的排列組合編譯選項(xiàng),去編譯不同的shader,雖然看起來(lái)是同一份shader代碼。這個(gè)轉(zhuǎn)換過(guò)程十分復(fù)雜,不必深究,我們知道如果出現(xiàn)permutation之類(lèi)的編譯選項(xiàng),則同一份shader代碼可能編譯出不同的shader即可。
3.3
FShader怎么做反射
FShader沒(méi)有像Unreal其他UObject類(lèi)的反射,用UPROPERTY就拿到它的反射,而是通過(guò)DECLARE_TYPE_LAYOUT宏,單獨(dú)實(shí)現(xiàn)的一套反射機(jī)制。
圖3.3.1 反射宏
比如這里FShader的成員變量, 只要加了這個(gè)LAYOUT_XXX,就有反射信息。它是怎么做到的呢, 我們展開(kāi)一個(gè)LAYOUT_FIELD 宏。
圖3.3.2 宏展開(kāi)
首先定義Bindings成員變量本身,然后定義了一個(gè)新的模板類(lèi)InternalLinkType,即記錄類(lèi)型, 和用offsetof拿到偏移量。
最終達(dá)到的目的是:只要拿到FShader的指針,加上這個(gè)Offset后,就拿到了這個(gè)Bindings成員變量的地址,再進(jìn)行到FShaderParameterBindings的內(nèi)存轉(zhuǎn)換,就能拿到這段內(nèi)存了。
LAYOUT_FIELD 的信息收集,就是收集Name、收集成員變量的指針、收集其他信息。
總結(jié)
本篇內(nèi)容介紹了Unreal 的封裝方法,講解了在Unreal中如何實(shí)現(xiàn)shader的封裝以及Fshader的反射。如果想了解更多關(guān)于封裝調(diào)用,形成完整渲染pass的信息,歡迎發(fā)送郵件至mkt@eptcom.com聯(lián)系我們。