本文使用的是UE4.19的源碼,介紹的也是4.19之前的管線,DrawingPolicy和DrawList會在4.22後被幹掉。但是我後面會持續更新,我後面有寫一篇MeshDrawPipline的文章。
這篇介紹的是4.21之前的管線,可以看完這篇再看MeshDrawPipline。如有錯誤,還請各路巨佬斧正。
4.21之前的虛幻渲染管線(使用Policy和DrawList)
4.22重構後的虛幻渲染管線(使用MeshDraw)
虛幻4中,在渲染線程層真正負責繪製操作的是DrawingPolicy和DrawingPolicy Factory。如果我們要定製自己的渲染器並且想從底層優化我們的渲染的話,就要從這兩個東西下手。不過在開始研究這兩個東西之前我們需要先搞清楚虛幻的整個渲染框架,我這裏大概說一下我自己的總結。之前的文章我也有描述過,但是都沒有描述完整。這裏分幾個部分:
【1】Shader的生成
【2】DrawingPolicy繪製數據的管理
【3】整個繪製過程
在開始之前,一定要明確的幾個概念。
(1)Shader一定是在使用之前就編譯好的(未來不知道會怎樣),從某種角度上說shader可以認爲是預編譯好的資源,不同的程序不同的環境平臺我們需要不同的Shader,也就是說我們一個效果會有很多個shader,要用的時候需要根據不同條件去取對應的出來。
(2)Shader C++類和Shader一定要區分開。Shader C++類是在CPU端管理控制shader的,shader是在GPU上跑的。留言區有位朋友質疑我都文章,就是因爲他沒有把這個概念明晰導致的。
(3)Shader的定義,編譯,使用是三個分開的過程,不能混在一起看作是一套連貫的線性過程。
首先是shader的生成。虛幻4的shader C++類主要分了三個類型,Material Shader Mesh Material Shader,Global Shader。總得來說MaterialSHader C++類和MeshMaterialShader C++類允許多份實例,GlobalShader C++類的實例只允許存在一份。
我們的材質編輯器負責填充一個叫Material Template的函數模板,當我們點擊Compile的時候,材質編輯器的材質節點便會填充這個MaterialTemplate.usf生成一個函數庫。這還沒完compile操作還會根據材質編輯器裏的各種宏,各種繪製狀態,遊戲的高中低配分級等條件編譯出很多份Shader,放在FMaterial的ShaderMap中。有一個宏材,質編輯器就會編譯出兩份shader,有兩個宏,就會編譯出4個,呈指數型上漲(先這麼簡單理解)。
(1)GlobalShader
感覺這樣直接說Material Shader太抽象了,我們先自己實際動手敲一個Golobal Shader繪製過程出來吧,自己調用Draw。敲完這個例子之後再進行下一步就會豁然開朗了。首先在引擎裏新建三個文件(紅圈圈起來的這三個。分別是SkyRender.usf;SkyRender.h;SkyRender.cpp)
我們先打開SkyRender.usf
敲入如下代碼:
#include "Common.ush"
uniform float4x4 UnityVP;
uniform float3 TestColor;
void MainVS(
in float3 InPosition : ATTRIBUTE0,
out float4 Position : SV_POSITION
)
{
Position = mul(float4(InPosition, 1.0f), UnityVP);
}
void MainPS(
out float4 OutColor : SV_Target0
)
{
#if SAMPLELEVEL == 0
OutColor = float4(1, 0, 0, 1);
#endif
#if SAMPLELEVEL == 1
OutColor = float4(0, 1, 0, 1);
#endif
#if SAMPLELEVEL == 2
OutColor = float4(0, 0, 1, 1);
#endif
}
我們包含了Common.usf。在Common.usf中會幫我們包含大量需要的輔助設置,從C++傳上來的宏,一些函數等等。然後我們聲明瞭一個叫UnityVP的矩陣和一個TestColor的顏色。然後就是我們的頂點着色器了,這裏直接把物體從世界空間變換到投影空間。最後是我們的像素着色器,我們這裏根據宏輸出不同的顏色。
然後打開SkyRender.h文件,敲入如下代碼:
#pragma once
#include "CoreMinimal.h"
#include "StaticMeshVertexDataInterface.h"
#include "StaticMeshVertexData.h"
#include "RenderResource.h"
struct FDebugPane
{
FDebugPane();
~FDebugPane();
void FillRawData();
void EmptyRawData();
void Init();
TArray<FVector> VerBuffer;
TArray<uint16> InBuffer;
uint32 Stride;
bool Initialized;
uint32 VertexCount;
uint32 PrimitiveCount;
FVertexBufferRHIRef VertexBufferRHI;
FIndexBufferRHIRef IndexBufferRHI;
};
void FDebugPane::FillRawData()
{
VerBuffer = {
FVector(0.0f, 0.0f, 0.0f),
FVector(100.0f, 0.0f, 0.0f),
FVector(100.0f, 100.0f, 0.0f),
FVector(0.0f, 100.0f, 0.0f)
};
InBuffer = {
0, 1, 2,
0, 2, 3
};
}
我們這裏只是做了一個FDebugPlane的數據封裝,封裝了初始化操作和頂點緩衝數組和索引緩衝數組。
然後打開SkyRender.cpp,敲入如下代碼:
#include "SkyRender.h"
#include "CoreMinimal.h"
#include "SceneRendering.h"
#include "RHICommandList.h"
#include "Shader.h"
#include "RHIStaticStates.h"
#include "ScenePrivate.h"
template<uint32 SampleLevel>
class TSkyRenderVS : public FGlobalShader
{
DECLARE_SHADER_TYPE(TSkyRenderVS, Global, /*MYMODULE_API*/);
private:
FShaderParameter Unity_VP;
public:
TSkyRenderVS(){}
TSkyRenderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
Unity_VP.Bind(Initializer.ParameterMap, TEXT("UnityVP"));
//VertexOffset.Bind(Initializer.ParameterMap, TEXT("VertexOffset"));
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("SAMPLELEVEL"), SampleLevel);
}
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true;
}
static bool ShouldCache(EShaderPlatform Platform)
{
return true;
}
virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
Ar << Unity_VP;
return bShaderHasOutdatedParameters;
}
void SetMatrices(FRHICommandListImmediate& RHICmdList, const FScene *Scene, const FViewInfo *View)
{
SetShaderValue(RHICmdList, GetVertexShader(), Unity_VP, View->ViewMatrices.GetViewProjectionMatrix());
}
};
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderVS<0>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderVS<1>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainVS"), SF_Vertex);
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderVS<2>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainVS"), SF_Vertex);
template<uint32 SampleLevel>
class TSkyRenderPS : public FGlobalShader
{
DECLARE_SHADER_TYPE(TSkyRenderPS, Global, /*MYMODULE_API*/);
TSkyRenderPS() {}
TSkyRenderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
TestColor.Bind(Initializer.ParameterMap, TEXT("TestColor"));
}
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("SAMPLELEVEL"), SampleLevel);
}
static bool ShouldCache(EShaderPlatform Platform)
{
return true;
}
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true;
}
// FShader interface.
virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
return bShaderHasOutdatedParameters;
}
void SetUniforms(FRHICommandList& RHICmdList, const FScene *Scene, const FSceneView *SceneView)
{
SetShaderValue(RHICmdList, GetPixelShader(), TestColor, FVector(1, 0, 0));
}
private:
void SetTexture(FRHICommandList& RHICmdList, UTexture2D *noisetex)
{
}
FShaderParameter TestColor;
};
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderPS<0>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainPS"), SF_Pixel);
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderPS<1>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainPS"), SF_Pixel);
IMPLEMENT_SHADER_TYPE(template<>, TSkyRenderPS<2>, TEXT("/Engine/Private/SkyRender.usf"), TEXT("MainPS"), SF_Pixel);
FDebugPane DebugMesh;
template<uint32 samplvl>
void RenderInternal(
FRHICommandList& RHICmdList,
const FScene *Scene,
const TArrayView<const FViewInfo *> PassViews,
TShaderMap<FGlobalShaderType>* ShaderMap
)
{
}
void FSceneRenderer::RenderMyMesh(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo *> PassViews, int32 sgPipeLineQuality)
{
check(IsInRenderingThread());
TShaderMap<FGlobalShaderType>* ShaderMap = GetGlobalShaderMap(FeatureLevel);
// pso init
FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
SceneContext.BeginRenderingSceneColor(RHICmdList, ESimpleRenderTargetMode::EExistingColorAndDepth, FExclusiveDepthStencil::DepthRead_StencilWrite, true);
FGraphicsPipelineStateInitializer PSOInit;
RHICmdList.ApplyCachedRenderTargets(PSOInit);
PSOInit.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None, false, false>::GetRHI();
PSOInit.BlendState = TStaticBlendState<>::GetRHI();
PSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_GreaterEqual>::GetRHI();
PSOInit.PrimitiveType = EPrimitiveType::PT_TriangleList;
PSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector3();
static const uint32 SampleLevel = 2;
TShaderMapRef<TSkyRenderVS<SampleLevel>> Vs(ShaderMap);
TShaderMapRef<TSkyRenderPS<SampleLevel>> Ps(ShaderMap);
PSOInit.BoundShaderState.VertexShaderRHI = GETSAFERHISHADER_VERTEX(*Vs);
PSOInit.BoundShaderState.PixelShaderRHI = GETSAFERHISHADER_PIXEL(*Ps);
SetGraphicsPipelineState(RHICmdList, PSOInit);
for (int i = 0; i < PassViews.Num(); ++i)
{
const FViewInfo *ViewInfo = PassViews[i];
Ps->SetUniforms(RHICmdList, Scene, ViewInfo);
Vs->SetMatrices(RHICmdList, Scene, ViewInfo);
if (!DebugMesh.Initialized)
{
DebugMesh.Init();
}
RHICmdList.SetStreamSource(0, DebugMesh.VertexBufferRHI, 0);
RHICmdList.DrawIndexedPrimitive(DebugMesh.IndexBufferRHI, PT_TriangleList, 0, 0, DebugMesh.VertexCount, 0, DebugMesh.PrimitiveCount, 1);
}
}
FDebugPane::FDebugPane()
{
Initialized = false;
}
FDebugPane::~FDebugPane()
{
VertexBufferRHI.SafeRelease();
IndexBufferRHI.SafeRelease();
}
void FDebugPane::EmptyRawData()
{
VerBuffer.Empty();
InBuffer.Empty();
}
void FDebugPane::Init()
{
FillRawData();
VertexCount = static_cast<uint32>(VerBuffer.Num());
PrimitiveCount = static_cast<uint32>(InBuffer.Num() / 3);
//GPU Vertex Buffer
{
TStaticMeshVertexData<FVector> VertexData(false);
Stride = VertexData.GetStride();
VertexData.ResizeBuffer(VerBuffer.Num());
uint8* Data = VertexData.GetDataPointer();
const uint8* InData = (const uint8*)&(VerBuffer[0]);
FMemory::Memcpy(Data, InData, Stride * VerBuffer.Num());
FResourceArrayInterface *ResourceArray = VertexData.GetResourceArray();
FRHIResourceCreateInfo CreateInfo(ResourceArray);
VertexBufferRHI = RHICreateVertexBuffer(ResourceArray->GetResourceDataSize(), BUF_Static, CreateInfo);
}
{
TResourceArray<uint16, INDEXBUFFER_ALIGNMENT> IndexBuffer;
IndexBuffer.AddUninitialized(InBuffer.Num());
FMemory::Memcpy(IndexBuffer.GetData(), (void*)(&(InBuffer[0])), InBuffer.Num() * sizeof(uint16));
// Create index buffer. Fill buffer with initial data upon creation
FRHIResourceCreateInfo CreateInfo(&IndexBuffer);
IndexBufferRHI = RHICreateIndexBuffer(sizeof(uint16), IndexBuffer.GetResourceDataSize(), BUF_Static, CreateInfo);
}
EmptyRawData();
Initialized = true;
}
cpp文件中分爲三部分,第一部分是我們的VS和PS,然後分別用IMPLEMENT_SHADER_TYPE聲明shader,這裏分別聲明瞭三個
爲我們的宏每一種情況各聲明一個
要時刻記住的一點是,Shader是提前編譯好的,我們更具不同的宏的值去取不同的shader出來渲染。
這就是DrawingPolicy和DrawingPolicyFactory,ShaderPermutation之所以存在的原因之一。
第二部分就是我們的繪製函數部分了,要完成這一部分我們需要到SceneRendering.h的FSceneRenderer類上添加上我們的渲染函數。
然後在DefferredShadingRenderer.cpp的Render函數中加上我們自己的Pass
然後你就能在引擎裏看到我們自己的Shader直接渲染出的模型了通過調整宏的值能改變我們模型的顏色
GlobalShader的拿去過程還說比較簡單的,直接根據GlobalShader的類型就能直接從GlobalShaderMap裏拿到相應的GlobalShader
但是MaterialShader和MaterialMeshShader就比較複雜了,因爲這兩個shader是要給單個模型用的,可能一個模型就至少需要一個Shader,模型裏面還要分動態模型和靜態模型,處理情況就非常複雜了。但是我們只要類比上面那個globalshader的例子來理解就會好理解很多。
總得來說我們的繪製需要對shader做兩件事情,就是對shader進行編譯然後放到一個容器裏(ShaderMap),然後我們要繪製的時候需要從這個容器裏把對應的shader取出來。
但是想要實現這個操作卻異常麻煩,首先我們要考慮遊戲的高中低配,在高畫質低畫質下的shader肯定是不一樣的。然後是頂點格式,骨骼模型和靜態模型的頂點數據肯定是不一樣的。然後是各種各樣的宏啦,混合模式,我們都需要不同的shader。
這時候DrawingPolicy和DrawingPolicyFactory就誕生啦。DrawingPolicy就是負責管理這些各種情況的,它負責具體的Drawing,而PolicyFactory則是負責組合各種數據來生成DrawingPolicy的。類比上面的例子,就是把上面的繪製管線的各種狀態的設置,宏的set,等操作封裝在一個名叫“XXXDrawingPolicy”的數據結構裏。至於爲啥要這麼做,後面會詳細闡述,這裏先留一個概念。
現在我們對虛幻的Shader的組織架構有了大概的瞭解,下面就來詳細研究下虛幻的MaterialShader的組織方式。
(1)MaterialShader
找到BasePassRendering或者MobileBasePassRendering我們總是可以看到一大片宏
我們全局查找這些宏你將會發現:
這些IMPLEMENT_MATERIAL_SHADER_TYPE宏到底在做什麼呢,我們跟進去就可以看到
它在實例化shader類的實例。這裏需要區分shader的C++類和shader,這非常重要,這個概念初學者還是很容易搞混或者忘記區分它們最後搞成一堆。
這個宏會把shader文件的路徑,類型,shader對應的DrawingPolicy全部和C++shader類綁定好。這也是爲什麼能夠使用Drawing Policy能夠在shadermap中查找到對應shader的原因,因爲是做過綁定的。
現在有了shader類和shader類的實例,下面就要來聊一下Policy了,如果是非常簡單的渲染器,一般一個shader類管理一個shader其實就足夠了。虛幻引入DrawingPolicy和DrawingPolicyFactory的目的其實是爲了垮平臺和實現方便編寫Shader。我們如果按照一個Shader C++類管理一個shader,一個Shader對應一個Shader文件。這實在太低效了,我們遊戲中需要很多shader。所以我們使用宏來開關shader中的某一部分。
在編譯Shader前,Shader C++類就需要先把這些宏設置好,決定shader中那些代碼會被編譯到最終的shader文件中,哪些不會。這樣我們就可以一個Shader C++類對應一個Shader C++類實例,對應很多Shader文件了。這樣看起來好像已經滿足我們的需求了,但是這又帶來了其它問題,我們現在在C++層仍然需要寫大量代碼,爲了增加自由度,我們可以把C++shader類的實例化過程再抽象一次。我們把shader c++類中的draw和各種狀態設置操作抽象出來,這樣通過不同的組合,就能自由創建出更多的Shader C++類實例,而抽象出來的這部分代碼就是DrawingPolicy和DrawingPolicyFactory。
現在準備好了Shader C++類和Shader C++類的實例,下一步就是編譯生成Shader了。下面是我跟了一遍Shader編譯的隨筆。
從上面鏈接的文章中我們可以大概知道虛幻shader的編譯過程。現在可還沒到繪製過程。我們重點關注FMaterialShaderMap::Compile這個函數
首先看到下面這行代碼
會根據不同的頂點格式創建很多ShaderMap,一個頂點格式對應一套ShaderMap。然後在這段代碼下面可以看到BeginCompile
這個函數會把FetureLevel和MaterialEnvironment傳進去。Feturelevel就是各個平臺信息,MaterialEvironment就是各種宏,我前面有篇增加新的光照模型的文章就有在改這個MaterialEnvironment的設置。
在BeginCompile中就會把Shader C++類拿出來編,下面就是在for循環拿到所有用Shader宏實例化的Shader C++類實例
在Shader C++類實例和與之綁定的DrawingPolicy的共同努力下,最終編譯成GPU用的Shader文件。並且把這些shader文件放在shadermap中。
【2】DrawingPolicy繪製數據的管理,shader的使用
現在已經大概瞭解了整個Shader和繪製的關係原理,那麼下面就進入MaterialShader的正題。
使用拿出主要分兩部分,一部分是staticmesh,一部分是dynamicmesh。他們倆的方式方法不同。StaticMesh會沿着DrawList來繪製,當我們把模型從資源瀏覽器裏添加到場景裏時,就會調用Render Scene的AddPrimitive函數,然後在這個函數種會調用Scene->AddPrimitiveSceneInfo_RenderThread(RHICmdList, PrimitiveSceneInfo);然後會調用FPrimitiveSceneInfo的AddToScene,然後會調用PrimitiveSceneInfo的AddStaticMeshes然後會調用FStaticMesh的AddToDrawLists然後這裏會根據情況把FStaticMesh加入到各個DrawingPolicyFactory裏面各個Drawing PolicyFactory又會調用AddStaticMesh函數,這裏以FDepthDrawingPolicyFactory的AddStaticMesh爲例這裏就會完成把我們的模型數據加入到FScene的各種DrawList裏面。靜態模型Draw的時候就直接調用DrawList進行繪製,在DrawList裏的DrawingPolicy就已經初始化好了。
DynamicMesh則不同,在繪製DynamicMesh之前會先創建DrawingPolicy然後再渲染
再這個構造函數中就對這個Policy的shader進行了初始化
這個函數就把Policy需要的所有Shader從Material中拿出來,跟進去將可以看到
根據各種符號拿相應的Shader出來,我們的ShaderMap在之前是綁定好生成好的,只要按照存入的規則,我們就可以輕而易舉把Shader取出來用。
可以看到這裏在拿Shader,拿到Shader之後Policy調用Draw方法,RHIComList再調用更底層。最終我們的Mesh就這麼被繪製出來了。因爲Drawingpolicy就是個對繪製邏輯的封裝,所以你可以再源碼裏找到各種各樣的名叫“XXXXDrawingPolicy”的東西,有的有共同的父類,有的Drawing Policy卻沒有共有的父類。
到了這裏,知道了Shader是如何生成的,如何使用的。但是還是留下很多疑惑,但核心疑惑仍然是,虛幻到底是怎麼把這些資源這些類組合到一起的。
我們的shader全部是運行之前就生成編譯好的。我們再來仔細研究下StaticDrawList。StaticDrawList被使用在繪製靜態物體上。這裏需要弄清楚的是,什麼是動態物體什麼是靜態物體,千萬不要以爲靜止不動的物體就叫靜態物體。準確地說,繪製狀態不發生改變的物體叫靜態物體。
這個StaticDrawList裏面儲存的DrawingPolicy。DrawingPolicy可以用來在shadermap中查找到對應的shader,於是我們繪製時需要使用的Shader有了。現在還差繪製的幾何數據,VB和IB。當我們在場景編輯器裏把模型拖進場景的時候,這時候幾何數據就會用DrawingPolicyFactory創建一個DrawingPolicy。DrawingPolicyFactory會創建數個DrawingPolicy,這些DrawingPolicy會分享一個batchmesh。
在繪製之前會先sort這些DrawingPolicy,要繪製的時候直接取出來用就可以了,DrawingPolicy和ShaderMap是匹配的(shader C++類的實例匹配好的),可以用DrawingPolicy查到shader。
【3】虛幻4整個繪製過程
首先包含了兩個階段,一個是Editor階段一個是Runtime階段,Editor階段就是我們平時打開編輯器的階段,這個階段主要是準備渲染資源,模型貼圖動畫的導入,場景的搭建。對於渲染器來說,場景的搭建過程其實是DrawList的構建過程,各種LightMap,SH的烘焙過程等等。這個階段我們會爲渲染器準備各式各樣的資源,包括但不限於模型貼圖數據,還有剛纔提到的靜態繪製列表等等。
然後就是Runtime階段。敲過渲染器的都知道,我們需要一個Render才能開始渲染啊。管理整個虛幻4渲染的是一個SceneRender的類,大的源頭應該從它開始
可以看到目前虛幻4只提供了兩個渲染器,一個是Mobile的,一個是Defferred的
如果嫌少,可以自己再創建一個渲染器也可以的
渲染器創建好以後會每幀調用它的Render函數以延遲渲染器爲例
這裏是在爲渲染做各種準備,SceneContex是從GBuffer的RenderTexturePool抽出來的,它就是一坨GBuffer
然後繼續往下走依然是一大坨準備工作,需要注意那個InitViews函數,它在抽取各種渲染數據,我們的RenderProxy的GetDynamicData就是在這裏被抽取的。
再往下就是在渲染各種Pass了
這些pass裏有些是用MaterialMeshShader繪製的,有些是用GlobalShader繪製的。要想繪製完一個完整的模型需要很多pass很多shader共同努力才能完成。這點需要和Mobile管線或者Unity的傳統認知分開一下。
在這些pass中着重看一下BasePass
BasePass會走RenderBasePassView
這裏就可以看到,渲染分渲染靜態模型和動態模型了
會根據從材質裏抽出的數據來分配渲染順序
然後調用在Editor階段就已經準備好的DrawList來繪製
根據DrawingPolicy的類型彈出正確的DrawingPolicy
然後根據各種ID各種Link把我們要的Policy彈出來繪製,整個過程可能有點繞。
Dynamic分支在第【2】部分就已經分析過了這裏就不贅述了。
還有很多小細節還需要慢慢理解,不過大體就是上面那樣了。最後歡迎給我留言一起討論