材質(Material) 是可以應用到網格物體(Mesh)上的資源,用它可控制場景的可視外觀。從較高的層面上來說,可能最簡單的方法就是把材質視為應用到一個物體的"描畫"。但這種說法也會產生一點點誤導,因為材質實際上定義了組成該物體所用的表面類型(質感)。您可以定義它的顏色、它的光澤度及您是否能看穿該物體(半透明)等等。
在這裡是默認大家對使用材質系統擁有基本的基本操作經驗,所以本文並不會對一般的簡單操作做任何的解釋和說明,如果大家想對Unreal材質系統的基本操作感興趣的話,參考官方文檔。https://docs.unrealengine.com/zh-CN/Engine/Rendering/Materials/index.html
對於某些高端的材質TA向的技巧和經驗總結以後會在之後的文檔中進行總結和歸納。
因此,本文的重點主要是來分析材質系統的運行邏輯和手段,從自動的代碼生成到生成變體的過程,最終到如何和幾何信息綁定提交渲染的一整套的流程。
此文章是之前發布在我的CSDN中,所以圖片會有水印。
由於後文即將闡述的編譯功能或者變體功能更加的抽象和難以理解,所以我們需要一個比較直觀的切入來審視Unreal的整個材質系統。
我希望呈現一種較為直接的介紹模式,這一切就在我們打開一個Materal.asset開始。其對應於代碼中的UMaterial。
1.1 PBR和它的輸出當我們打開材質編輯器,最開始我們能最直接看見的就是我們的材質的輸出。如下圖
圖片是針對迪斯尼的PBR參考,而unreal的則不是如此。
對應代碼中,是對應的各種FXXXMaterialInput。材質輸出屬性在UE4中的基類叫FMaterialInput,它有一些子類用於表達具體的輸出屬性的具體數據類型,如FColorMaterialInput,FScalarMaterialInput,FVectorMaterialInput等。
UMaterial中對應的材質輸出屬性和材質編輯器上的輸出節點屬性幾乎是一一對應的(注,顯示出來的名字可能會和變量量不同,因為名字因材質類型,光照模型而顯示不同的名字),完整定義如下:
FColorMaterialInput BaseColor;FScalarMaterialInput Metallic;FScalarMaterialInput Specular;FScalarMaterialInput Roughness;FVectorMaterialInput Normal;FColorMaterialInput EmissiveColor;FScalarMaterialInput Opacity;FScalarMaterialInput OpacityMask;FVectorMaterialInput WorldPositionOffset;FVectorMaterialInput WorldDisplacement;FScalarMaterialInput TessellationMultiplier;FColorMaterialInput SubsurfaceColor;FScalarMaterialInput ClearCoat;FScalarMaterialInput ClearCoatRoughness;FScalarMaterialInput AmbientOcclusion;FScalarMaterialInput Refraction;FVector2MaterialInput CustomizedUVs[8];FMaterialAttributesInput MaterialAttributes;FScalarMaterialInput PixelDepthOffset;FShadingModelMaterialInput ShadingModelFromMaterialExpression;1.2頂層設計
至此我們先來高屋建瓴的看一下與Material相關的類,來更加清楚地了解其頂層設計。
1.2.1UMaterialInstance和UMaterialInstanceDynamic材質實例,是我們在編輯器中經常使用的一種資源,通過將通用的材質統合起來,有效的減少材質的數量。使用上,如果材質之間就只有貼圖或者參數不同的話,就可以構建一個基礎的材質,然後將需要變化的材質進行參數化。並創建材質實例以應對不同的情況。
只有UMaterial材質模板帶有可編輯的節點圖並可據此生成對應的Shader組合,而UMaterialInstance材質實例則只需要引用UMaterial對應的Shader.UMaterialInstance只能修改材質模板暴露出來的材質參數。
有幾點是非常需要注意,其一是UMaterialInstance的繼承關係,UMaterialInstance並不是繼承自UMaterial而是和UMaterial一樣繼承自UMaterialInterface。這和我們在使用時的感覺是不太一樣的,因為我們在使用時一般都是使用UMaterial來生成UMaterialInstance。
這種設計模式的目的應該是非常的清楚,因為UMaterialInstance是可以用另一個UMaterialInstance來進行創建的。具體的實現細節就需要深入到代碼中來細細品來。
UMaterialInstanceDynamic實際繼承的是UMaterialInstance,這是我們在程序運行時經常使用的一種。
1.2.1FMaterial這是沒有GC的UMaterial的剝離產物。
1.2.2FMaterialResource拋開PhysicalMaterial(UMaterial中包含的成員變量),UE4中的UMaterial包含在不同的硬體條件(FeatureLevel)和不同的材質質量(MateialQualityLevel)設定下的不同材質表現,同時它可能它在引擎的其它子系統下也可能有自己的特殊表現。如LightMass下就和實時渲染的材質參數不同。
這些表現方式被封裝為FMaterialResource類,FMaterialResource是UMaterial為指定FeatureLevel和MateialQualityLevel後的具體表現形式。
FMaterialResource包含用於渲染的Shaders、Shaders參數、RenderStates、LightingMode等數據。因LightingMode在UE4中不可定製只可選擇內蘊的幾種固定模型,故其可看作一種特殊的RenderState,在此前提下FMaterailResource簡化為ShaderMap + Shaders參數 + RenderStates。其中Shader參數和RenderStates來源於對材質本身的引用數據,ShaderMap則來源於編譯材質節點圖所生成的Shader變種。
1.2.3FMaterialRenderProxyFMaterialRenderProxy是FMaterial用於渲染線程的代理,它可以透過FMaterail和UMaterialInterface訪問到Shader、渲染狀態,光照模型等所有用戶設置好的材質參數。
1.3材質節點當我們了解了頂層的設計框架,我們接下來將深入到另一個代碼生成的領域當中,來探究材質編輯器的連連看是如何工作的。
在所有的材質表達式中,我們會將所有的表達式存入Expressions。
類似於藍圖,材質編輯器中典型的節點包含節點本身,節點的輸入,節點的輸出三個部分。特殊情況下,一些節點沒有輸入,如const數據,各類parameters,內置的inputs節點等等。
UMaterialExpression是材質節點的基類,UMaterialExpression繼承自UObject。其定義了通用的材質節點的屬性和方法。
UMaterialExpression的主要屬性 :
UEdGraphNode* GraphNode; class UMaterial* Material; class UMaterialFunction* Function; uint32 bIsParameterExpression:1; uint32 bShaderInputData:1; TArray<FExpressionOutput> Outputs;UMaterialExpression的主要方法
virtual int32 Compile(class FMaterialCompiler* Compiler, int32 OutputIndex) { return INDEX_NONE; }FExpressionInput是材質節點的輸入,它是個結構體,持有一個UMaterialExpression指針,並指定它自己取的是該Expression的哪個輸出。它基本上就是對UMaterialExpression的一個簡單的封裝。
FExpressionOutput是材質節點的輸出,同樣是個結構體,它比Input簡單,結構體裡只有名字和Mask。Mask用於Mask輸出的結果,可能同一個結果會顯示為多個輸出,如TextureSampler2D有RGB,R,G,B,A四個輸出,它們都是對TextureSample出來的RGBA進行不同分量Mask的結果。
1.3.1材質節點的種類內置輸入節點 ,顯示為紅色Tile的節點,輸入類節點本身沒有輸出接口,這些節點可能來源於前一Shader階段的輸出數據(如TexCoord[0]),也可能來源於引擎內置的UniformParameter(如Time節點)。
內置函數和運算符節點,顯示為暗綠色的節點,內置的函數節點可能來源於HLSL語言層面的原生函數(如sin,cos),也可能來源於引擎自己在usf中定義的標準函數庫(如sinFast,cosFast)等。
常數節點,這類節點同樣顯示為亮綠色。如Constant、Const2Vector,Const3Vector,StaticBool等節點
可變參數節點(UniformParameter) ,這類節點顯示為亮綠色,其來源於用戶在運行時可修改的Shader參數。如ScalarParameter,TextureSampleParameter2D等。TextureSample節點雖然在編輯器語義下是常數類節點,但實際上它在編譯為Shader參數時同樣為UniformParameter,實際上所有Texture相關的節點在編譯後都表示為UniformParameter。
Static節點族:StaticBool為常數節點,StaticBoolParameter是允許材質instance修改的常數節點(因為修改節點後會導致shader重編),StaticSwitch類似於在usf中進行宏定義,其結果是只編譯通過StaticSwitch測試的那個分支的代碼,另一個分支會被直接丟棄。StaticSwitchParamter是在instance中可以修改可通過分支的StaticSwitch,同樣它的修改會導致shader重編。
FeatureLevelSwitch和QualitySwitch節點 :這兩個節點直接對應UMaterial中的材質切換條件:根據硬體條件和質量等級切換不同品質和實現的材質。
關於if節點。測試了一下,是變為lerp,兩邊都會進行計算,所以其實在材質編輯器裡寫if並不是真的if,如果有需要的話還是自己去寫custom。測試了下niagara裡面寫的是真的if。
不存在流程跳轉節點,所以循環節點也就無從談起了——在材質編輯器中找不到for和while節點的原因也在於此。當然我們也可以在Custom節點裡面去寫循環
還有一個節點叫ShadowSwitch,通常情況下我們的Shadow和光照應該是一致的(World Offset)但是我們可以通過這個節點來控制不一樣的行為。
二.材質的編譯本節將要討論UE4從材質藍圖生成原始的HLSL代碼的基本流程,這兒的材質特指的Surface Domain類材質,其對應的Shader種類為MeshShader,只因這一類材質應用最為廣泛,可用於描述任何3D Primitive表面屬性,一個遊戲中可見的90%以上材質都屬此類。
2.1材質編譯的入口和時機對於UMaterial,其生成Shader的入口函數有二,
1.一個用於資源Cooking,名為CacheResourceShadersForCooking。2.一個用於材質編輯和引擎運行時渲染,名為CacheResourceShadersForRendering,
在不同的階段,如果我們尋找一個shadermap在內存或者DDC中沒有找到,那麼就會調用這兩個函數。
我們僅針對一下CacheResourceShadersForRendering來做一下比較深入的探討。首先的問題就是其調用時機是什麼。
1.在反序列化時會進行調用。
2.當我們打開材質編輯器中的實時預覽按鍵時,其會在每一次的改動後進行調用。其始於 UMaterial::PostEditChangeProperty。
3.當我們沒有勾選實時預覽時,我們點擊apply按鍵才會進行UMaterial::PostEditChangeProperty的調用。
因此,我們將調用接口和時機討論完後,我們將進行更加細節的探討,討論這個函數到底幹了一些什麼我們關心的事情。
2.2材質編譯做了什麼當我們發生了材質的更改,接下來會發生如下事件
1.FlushRenderingCommands(),會被直接調用,強制之前的命令緩衝渲染完畢,這裡確保線程安全。之後調用2.CancelOutstandingCompilation(),強制所有的目前的編譯工作取消,這也是很自然的意見事情。3.之後我們調用CacheResourceShadersForRendering(bRegenerateId);
我們正式進入到CacheResourceShadersForRendering裡面
2.2.1RebuildShadingModelField這裡顧名思義是用來重新編譯我們的ShadingMode,這裡需要注意的是,處理我們默認的shading model外,還有一個MSM_FromMaterialExpression,這個是需要特殊處理的。
目前這個階段僅僅是識別,並沒有任何其他的處理。
2.2.2FlushResourceShaderMaps在這裡我們需要更加深入的講解一下我們的FMaterialResource。
FMaterialResource是FMaterail的子類,用於UMaterial的渲染,FMaterialResource代表的是從一個截面(在某個Render API FeatureLevel、指定的平臺下、指定材質品質預設下)時它對應的UMaterial的具體呈現。
雖然來說我們這裡的FMaterialResource並不是函數名稱關於ShaderMaps代表之一,不過我們並不打算在這裡進行對變體這一特性的深入探討,我們將在下一節,進行更加細緻和縝密的探究。
回歸正題,我們FlushResourceShaderMaps調用的FMaterial::ReleaseShaderMap()函數,實際是對於我上圖中我們對不同的質量不同的RHIFeaturelevel的二維數組中的每個FMaterialResource的調用。
ENQUEUE_RENDER_COMMAND(ReleaseShaderMap)([Material](FRHICommandList& RHICmdList) { Material->RenderingThreadShaderMap = nullptr; });2.2.3UpdateResourceAllocations
從這裡我們很明顯的看到,關於對MaterialResources的空間分配和設置更新
2.2.4CacheShadersForResources在這裡,我們會所有的單個FMaterial執行Shader生成的函數為CacheShadersForResources,CacheShadersForResources先會重建材質函數依賴信息、材質收集器依賴信息,並把此材質(或材質函數)所依賴的輸出和輸入紋理填充到依賴列表裡。
2.2.4.1搜集階段1.RebuildMaterialFunctionInfo:這裡會將所有的材質表達式,進行篩選2.RebuildMaterialParameterCollectionInfo3.AppendReferencedTextures這裡會將材質中所有引用的貼圖進行收集
不管如何,這個階段也僅僅只是搜集填充MaterialFunctionInfos,MaterialParameterCollectionInfos,InOutTextures,CurrentFunction
2.2.4.2CacheShaders(ShaderPlatform, TargetPlatform);
這裡我們就進入了FMaterial類中。在這裡,我們的不會有static parameters和相應的ShaderPlatform,TargetPlatform宏的開關問題,因為這裡都會被指定。
FMaterialShaderMapId,是唯一標識FMaterialShaderMap的ID,內置變量很多,列出比較重要的幾個
1.FSHAHash CookedShaderMapIdHash;2.FGuid BaseMaterialId;3.EMaterialQualityLevel::Type QualityLevel;4.ERHIFeatureLevel::Type FeatureLevel;
在這個FMaterialResource中存在兩個FMaterialShaderMap指針,這兩個指針一個用於遊戲線程(GameThreadShaderMap),一個用於渲染線程(RenderingThreadShaderMap),實際上這兩個ShaderMap指針指向的是同一個FMaterialShaderMap對象。
之後,會把所有的依賴注入給FMaterialShaderMapId,當然數據僅僅是TArray。
隨後判斷,如果自己本身是MaterialInstance,那麼我們將要把配置的StaticParmeterValue進行打開關閉。
得到一個FMaterialShaderMapId後,繼續進行調用另一個FMaterial::CacheShaders來進行後續的操作
2.2.4.3CacheShaders(ShaderMapId, Platform, TargetPlatform);
這裡會區分是否是第一次還是擁有基本的cache,我們現在僅討論第一次創建的情況。我們將排除其他非常良好的情況。我們看需要進行編譯的地方。BeginCompileShaderMap
2.2.4.4BeginCompileShaderMap
真正的編譯環節是通過FHLSLMaterialTranslator來完成初始的翻譯工作,然後,再進行編譯。
2.2.4.4.1翻譯階段
直接的調用MaterialTranslator.Translate();
FHLSLMaterialTranslator類是材質由表達式編譯(翻譯)成HLSL的核心類,實現了幾乎所有材質表達式的代碼生成功能:幾乎每個UMaterialExpression的子類在FHLSLMaterialTranslator類中都有一個唯一的函數與之一一對應。這樣在UMaterialExpression的Compile函數被調用時步驟分為三步,先檢查表達式的合法性和前置條件,再處理表達式的輸入節點,最後把表達式的條件和輸入節點一起送入FHLSLMaterialTranslator與之對應的函數中進行HLSL代碼生成。
我們將要擴展材質編輯器的節點,就必須要處理翻譯這裡的流程。
這是一段非常獨立且極其龐大的代碼段,這裡並沒有對我們介紹其他部分會有影響,所以我們暫且不談。
2.2.4.4.1編譯階段
當我們得到翻譯結果後,我們將翻譯後的字節碼存儲為FString,然後增加一些path,或者是設置環境和平臺的操作後就進行編譯。
在編譯階段,我們需要再次更加深刻的去理解幾個概念。
三.材質的編譯材質必須支持應用於不同的網格類型,而這是通過頂點工廠來實現的。
我們來看一下我們的系統當中到底擁有多少的FVertexFactoryType
1.FGPUBaseSkinVertexFactory2.FGeometryCacheVertexVertexFactory3.FLandscapeVertexFactory4.FLocalVertexFactory5.FNiagaraVertexFactoryBase6.FParticleVertexFactoryBase7.FPointCloudVertexFactory8.FVectorFieldVisualizationVertexFactory
以上只是一級繼承關係,還有更多的二級繼承的類別。其中的每一個都是作用於不同的地方,但其實我們一般而言會進行使用的是則是FLocalVertexFactory。這是一般的mesh的不同渲染的頂點工廠。
其實我們可以更進一步的來看一下這些頂點工廠的不同,但是我們還是將這種細緻的差別比較放在shader篇更加的妙。我們在這裡僅僅是想讓大家知道我們的material不是僅支持FLocalVertexFactory的,其實它還要支持非常非常多的頂點工廠。
shadertype看似比較少,其實不是的。
FGlobalShaderType FMaterialShaderType FNiagaraShaderType FOpenColorIOShaderType FMeshMaterialShaderType這些只是內置的shaderType,但是,你只要在程序裡繼承FShader的shader,都會生成自己的ShaderType。所以,可以預見的是,shaderType的數量是極其巨大的。
這裡其實對unreal的shader有一個非常重要的思考,那就是對於我們日常見到的材質,其實都是生成的模板,也就是把連線連接到PBR節點的輸出當中,但是具體的如何使用,是通過shader中的代碼來使用的。所以如果不加限制,那麼變體的數量則會非常急速的增加。
例如,我們自己實現的噴射管線的shader
其中的一個函數ShouldCompilePermutation就是判斷FShaderType能給哪些生成變體的。
如果我們返回的是true,那它將對所有的場景中所有的material生成對應的變體!
FShaderPipelineType
這裡蘊含著和Mesh相關Rendering 的個數(如DepthRendering ShadowRendering,BasePassRendering)等等,對每一個材質,我們都會生成不同的生成陰影貼圖的pipline。具體的pipeline的組合數非常的多,例如
FMaterialResource不是持有一個Shader,而是持有在指定截面下此材質所有的Shader變體(Shader Permutations)。
影響變體多寡主要因素包括我們已經在上述講完了。
一個材質動輒150+的Shader Permations是很正常的事,所以在UE開發中,ShaderPermation Reduce對包體和運行效率均為不可迴避的優化項。