UE4 RHI與Render模塊簡解


  UE4中的RHI指的是Render hardware interface,作用像Ogre里的RenderSystem,針對Dx11,Dx12,Opengl等等平台抽象出相同的接口,我們能方便能使用相同接口對應不同渲染平台.

  和以前一樣,先簡單介紹一些類與文件的作用,我們有個抽象的了解.

  RHI.h :主要定義一些硬件平台的公共變量.

  一是 硬件支持項,如是否支持PF_FloatRGBA格式渲染目標,手機平台是否支持FrameBuffer拾取,支持體紋理,支持硬件合並渲染等等.

  二是 硬件變量,如最大Cube紋理數,陰影貼圖長寬最大值等等.

  三是 常見渲染定義,如FSamplerStateInitializerRHI紋理采樣,FRasterizerStateInitializerRHI柵欄化(填充格式,正方向定義,MSAA),FDepthStencilStateInitializerRHI逐片斷處理中的模板與深度,FBlendStateInitializerRHI逐片斷處理中的混合.FRHIDrawIndirectParameters/FRHIDrawIndexedIndirectParameters DrawCall中相關參數.

  DynamicRHI.h :包含FDynamicRHI接口定義,渲染所需求所有接口,創建buffer,創建紋理,設置着色器參數,UAV等,簡單來說,對應opengl,dx提供的渲染API,其DynamicRHI.cpp文件會根據平台(Windows,apple,android等等)來選擇加載合適的渲染平台(如Opengl,Dx,Vulkan等),在RHI模塊的private文件夾下,可能看到各個系統會如何選擇相應的渲染平台.  

  FRenderResource:定義接口如InitDynamicRHI /ReleaseDynamicRHI /InitRHI /ReleaseRHI /InitResource /ReleaseResource /UpdateRHI等渲染資源選擇實現函數.

  RHICommandList相關文件是我們講RHI主要需要講的,在這我們先來分析出現的各個類.

  FRHICommandBase: 主要定義一個函數指針,一個執行方法調用函數指針指向的函數。

  函數指針:二個參數(FRHICommandListBase,FRHICommandBase)CallExecuteAndDestruct:傳入自己FRHICommandBase到時函數指針指向的方向.

  FRHICommand: FRHICommandBase的一個模板子類,模板需要定義Execute方法,其方法只需要FRHICommandListBase,其會退化上面CallExecuteAndDestruct的FRHICommandBase參數,默認為自己.

  FRHICommand的模板具體化,對應SetRasterizerState/SetDepthStencilState/SetShaderParameter等等,幾乎所有渲染API都有對應的FRHICommand的模板具體化實現.

  FRHICommandListBase: 相應FRHICommandBase的鏈表實現,以及定義一些上下文如IRHICommandContext ,IRHIComputeContext ,並且有相關和RHI線程交互的API,RHI本身相應的FRHICommandBase與List都是存放在渲染線程中,RHI線程可以用於在渲染線程中同步執行異步的復雜操作,如壓入很多FRHICommandBase到渲染線程中執行,有些操作可以放入RHI線程中與渲染線程一起執行,在某段FRHICommandBase前,調用WaitForTasks等同步渲染線程與RHI線程,大家可以這么理解,RHI線程對於渲染線程就相當於渲染線程與游戲線程的關系,大家可以看我上篇UE4里的 渲染線程 ,看到如何在渲染線程里壓入RHI線程,如何用WaitForTasks與渲染線程同步等.

  FRHICommandList: 簡單來說,所有用於渲染API幾乎都有二種方法,一種是插入FRHICommandListBase鏈表,一種是直接調用相應渲染平台對應FDynamicRHI的實現,在這說下,我看了下OpenGLDrv相應的FDynamicRHI實現,相應API如SetShaderParameter, SetDepthStencilState等等,並沒有直接調用相應的OpenGL的API,而是把相關改動放入一個FOpenGLRHIState的結構中保存起來,等到DrawCall(如RHIDrawPrimitiveIndirect等)相關命令調用后,才把各個改動對應opengl的API調用起來,如上的glProgramUniform等.

  FRHIAsyncComputeCommandList: 多GPU的FRHICommandList實現。

  FRHICommandListImmediate: 直接調用相應渲染平台對應FDynamicRHI的實現,對比FRHICommandList,主要是創建資源這一塊的FDynamicRHI封裝,可以看到它的一些函數都是以Create開頭的。

  FRHICommandListExecutor: 簡單來說,管理FRHICommandListBase的幾個子類單例實現,方便查找到如上的FRHICommandListImmediate 與FRHIAsyncComputeCommandListImmediate 單例實現,一般我們看到渲染代碼里常見的如FRHICommandList/RHICmdList就是指的是FRHICommandListExecutor::GetImmediateCommandList().

  在這,關於RHI的就先簡單了解下,RHI主要調用都在渲染線程中,不過也可以使用FRHICommandListBase鏈表與RHI線程來實現一些同步異步操作。其中渲染模塊中FRHICommandList/RHICmdList一般是FRHICommandListExecutor::GetImmediateCommandList(),這個是直接調用相關FDynamicRHI實現,一般並不與RHI線程交互。

  介紹RHI模塊后,我們來看下渲染模塊的相關實現,在說下渲染模塊的實現前,簡單說下,UE4中大量用到C++ 的模版,除開自動生成各個分支代碼,還有二點,一是代替部分接口類,減少如虛函數表的性能,二是減少一些分支判斷,還是提高性能。但是會造成閱讀代碼比C#等語言驗證,主要在於有些模板你都不知道是那些類可以用等,還好,UE4里一般這種模板使用類都有相同的前綴或是后綴,我們可以記一些相同的前綴或后綴轉化成自己認為的接口實現。

  我們先看一段代碼,是OpenGLDrv實現的FDynamicRHI子類FOpenGLDynamicRHI的RHIDrawPrimitiveIndirect,簡接繪制多組圖元集。 

void FOpenGLDynamicRHI::RHIDrawPrimitiveIndirect(uint32 PrimitiveType,FVertexBufferRHIParamRef ArgumentBufferRHI,uint32 ArgumentOffset)
{
    if (FOpenGL::SupportsDrawIndirect())
    {
        VERIFY_GL_SCOPE();

        check(ArgumentBufferRHI);
    GPUProfilingData.RegisterGPUWork(0);

        FOpenGLContextState& ContextState = GetContextStateForCurrentContext();
        BindPendingFramebuffer(ContextState);
        SetPendingBlendStateForActiveRenderTargets(ContextState);
        UpdateViewportInOpenGLContext(ContextState);
        UpdateScissorRectInOpenGLContext(ContextState);
        UpdateRasterizerStateInOpenGLContext(ContextState);
        UpdateDepthStencilStateInOpenGLContext(ContextState);
        BindPendingShaderState(ContextState);
        SetupTexturesForDraw(ContextState);
        CommitNonComputeShaderConstants();
        CachedBindElementArrayBuffer(ContextState,0);

        // Zero-stride buffer emulation won't work here, need to use VAB with proper zero strides
        SetupVertexArrays(ContextState, 0, PendingState.Streams, NUM_OPENGL_VERTEX_STREAMS, 1);

        GLenum DrawMode = GL_TRIANGLES;
        GLsizei NumElements = 0;
        GLint PatchSize = 0;
        FindPrimitiveType(PrimitiveType, ContextState.bUsingTessellation, 0, DrawMode, NumElements, PatchSize);

        if (FOpenGL::SupportsTessellation() && DrawMode == GL_PATCHES )
        {
            FOpenGL::PatchParameteri(GL_PATCH_VERTICES, PatchSize);
        } 

        FOpenGLVertexBuffer* ArgumentBuffer = ResourceCast(ArgumentBufferRHI);


        glBindBuffer( GL_DRAW_INDIRECT_BUFFER, ArgumentBuffer->Resource);
        {
            CONDITIONAL_SCOPE_CYCLE_COUNTER(STAT_OpenGLShaderFirstDrawTime, PendingState.BoundShaderState->RequiresDriverInstantiation());
            FOpenGL::DrawArraysIndirect( DrawMode, INDEX_TO_VOID(ArgumentOffset));
        }
        glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0);
        
        FShaderCache::LogDraw(0);
    }
    else
    {
        UE_LOG(LogRHI, Fatal,TEXT("OpenGL RHI does not yet support indirect draw calls."));
    }

}
FOpenGLDynamicRHI::RHIDrawPrimitiveIndirect

  前面說過,FOpenGLDynamicRHI是在DrawCall時,才把各個改動對應opengl的API調用起來,所以在這,我們可以看到一個渲染的完整過程,當然大家使用過Opengl或是DX直接寫過程序也是一樣,首先設定渲染目標,混合,設定viewport,設定柵欄化,設定逐片斷處理(深度,模板),綁定Shader程序,設定shader紋理,設置shader參數,綁定VAO,設定VAO,DrawCall,嗯,就是這么個過程,無論UE4如何包裝,每次DrawCall就是如上順序處理。

  先說一下在渲染模塊里比較常見的類:

  后綴Parameters: 二個主要方法,一是Bind,簡單來說,對應一個或多個參數Parameter與Shader代碼里參數綁定,對應opengl里的API就是如glGetUniformLocation。二是Set,簡單來說,上面綁定后,我們就可以傳入參數的值到GPU里,對應opengl里的API就是如glUniform等等。

  模板類里的模板如果是后綴ParametersType,一般主要是指各個后綴為Parameters的類。

  如下一些類寫了些自己了解,后查找資料時發現UE4官方文檔里 着色器開發 有說,比我說的清楚。

  FVertexFactory: 用來表示頂點數據格式,頂點分布結構,頂點元素Buffer,DeclarationElementList數組,相關opengl的API如glVertexAttribPointer.從opengl3+來說,一般雖然可能有多個buffer,但是應該是在一個glgenbuffer中對應不同的區段而已。

  方法Set: 只是告訴對應opengl里各個buffer的起點與終點,相應的如OffsetInstanceStreams/SetPositionStream都是類似。

  FVertexFactoryType:表示網格類型,如 Local/Particle(三種sprite/beamtrail,mesh)/Landscape/GPUSkin等,

  FMeshBatch: 一般來說,是一組相同頂點格式,相同材質的模型,一般可以使用GPU的實例渲染,減少DrawCall.

  FShaderType: Global/Material/MeshMaterial (vertex/hull/demain/geomerty/pixel 一種)

  FGlobalShader: 全局shader,簡單來說,不和mesh與Material關聯,一般用於后處理,固定畫個方塊啥的,如處理特效這種。

  方法SetParameters: 設定Shader里的FViewUniformShaderParameters /FFrameUniformShaderParameters /FBuiltinSamplersParameters 參數。

  FMaterialShader: 特定於過程的着色器,它們需要訪問材質的某些屬性,因此必須針對每個材質進行編譯,但不需要訪問任何網格屬性。如FLightFunctionVS,FLightFunctionPS等。  

  對比FGlobalShader,增加一個重載的SetParameters,包含材質對Shader的設置。

  FMeshMaterialShader: 着色器是特定於過程的着色器,它們依賴於材質的屬性和網格類型,因此必須針對每個材質/FVertexFactory組合進行編譯。例如,TBasePassVS / TBasePassPS 需要對前向渲染過程中的所有材質輸入進行評估。

  對比FMaterialShader,增加一個方法SetMesh,添加FMeshBatchElement,FVertexFactory對shader的設置,對應VertexFactory的Parameters針對Mesh填充不同的頂點信息。 如GPUSkin,填充骨骼信息到相應的shader參數中, 如MeshParticle,填充動畫加速度 ,時間等。以及填充模型本身的FPrimitiveUniformShaderParameters等共有信息,如FPrimitiveUniformShaderParameters:localToworld ,worldTolocal ,objectBounds, LOD,FadeTimeScaleBias等。

  如下這些類表示渲染主要思路,預先一些相同的渲染方式,可以先緩存起來。

  FMeshDrawingPolicy: 整合渲染模型過程,從綁定Shader到調用DrawCall,各個子類對應不同的獨立着色器程序。

  1 初始化,根據需要生成或綁定各個對應的Shader.

  2 SetSharedState,設定和Mesh無關的Shader變量。

  3 SetMeshRenderState,設定和Mesh相關的Shader變量。

  4 DrawMesh 調用DrawCall.

  模板類里的模板如果是DrawingPolicyType,一般主要是指FMeshDrawingPolicy的各個子類。

  FUniformLightMapPolicy: 封裝和光照有關渲染的Shader參數設置。  

  方法SetMesh:綁定相應光照計算上Shader的參數,如使用GI預計算產生的間接光照圖信息,直接光照圖信息,天空圖AO等。

  TUniformLightMapPolicy: FUniformLightMapPolicy的模版子類,模版為ELightMapPolicyType,表示各種和光照有關,模版預生成多份代碼對應不同光照計算表示是否緩存,Shader預編譯指令。

enum ELightMapPolicyType
{
    LMP_NO_LIGHTMAP,
    LMP_CACHED_VOLUME_INDIRECT_LIGHTING,
    LMP_CACHED_POINT_INDIRECT_LIGHTING,
    LMP_SIMPLE_DYNAMIC_LIGHTING,
    LMP_LQ_LIGHTMAP,
    LMP_HQ_LIGHTMAP,
    LMP_DISTANCE_FIELD_SHADOWS_AND_HQ_LIGHTMAP,
    // Forward shading specific
    LMP_DISTANCE_FIELD_SHADOWS_AND_LQ_LIGHTMAP,
    LMP_SIMPLE_DIRECTIONAL_LIGHT_AND_SH_INDIRECT,
    LMP_SIMPLE_DIRECTIONAL_LIGHT_AND_SH_DIRECTIONAL_INDIRECT,
    LMP_SIMPLE_DIRECTIONAL_LIGHT_AND_SH_DIRECTIONAL_CSM_INDIRECT,
    LMP_MOVABLE_DIRECTIONAL_LIGHT,
    LMP_MOVABLE_DIRECTIONAL_LIGHT_CSM,
    LMP_MOVABLE_DIRECTIONAL_LIGHT_WITH_LIGHTMAP,
    LMP_MOVABLE_DIRECTIONAL_LIGHT_CSM_WITH_LIGHTMAP,
    // LightMapDensity
    LMP_DUMMY
};
ELightMapPolicyType

  TLightMapPolicy: Shader對應預編譯指令,是否緩存,模板為ELightmapQuality,有二個值,分別是LQ_LIGHTMAP,HQ_LIGHTMAP。

  模板類里的模板如果是LightMapPolicyType,一般主要是指TUniformLightMapPolicy/TLightMapPolicy的各個子類。

  如上一些基本比較重要的類就到此,在這我們重點說下FMeshDrawingPolicy這個類,從上面各個類的說明來看,可以看到把所有渲染基本類組合在一起,他的子類簡單說幾個,BasePassRendering ,CapsuleShadowing ,DepthRendering ,ForwardBasePassRendering ,VelocityRendering等等,還有別的帶Rendering的渲染,如DistortionRendering ,DeferredShading ,DecalRendering,ShadowRendering等等雖然和FMeshDrawingPolicy不同,但是過程其實真差不了多少。 在每個Rendering中,都有對應的VS,PS,HS等,這些根據需要分別從上面所說的FGlobalShader /FMaterialShader /FMeshMaterialShader繼承,簡單來說,后處理特效針對渲染目標的一般從FGlobalShader繼承,只針對Material不和具體Mesh有關的用FMaterialShader,最后針對模型渲染的從FMeshMaterialShader繼承。

  按BasePassRendering說下,只簡單渲染emissive color與light map,對應的FMeshDrawingPolicy子類為TBasePassDrawingPolicy ,如上所說,針對Mesh產生的都繼承與FMeshMaterialShader生成的VS,PS等,因為光照有影響,我們看到相應的Shader都對應模版LightMapPolicyType,用於生成正確的Shader對應預編譯指令,如有無光照,光照質量,靜態或動態,陰影類型等。下面還定義一些與BasePassRendering相關的parameters,如天空盒相關參數,如上TBasePassDrawingPolicy在構造函數中得到或是生成上面的VS,PS,然后在SetSharedState時針對VS,PS設定參數,然后調用SetMeshRenderState針對每個FMeshBatch設定和Mesh有關的參數,然后提交DrawCall.

  每個DrawingPolicy中,對應VS,PS等對應文件可以通過宏IMPLEMENT_SHADER_TYPE查看。

  本文本來還准備更詳細講述一個基本的Rendering的過程,但是新項目時間緊,只是暫停查看,后面會仔細介紹一個完整流程,從陰影渲染,前向或是后向渲染選擇一部分來詳細介紹,包含大部分參數的含義與作用,不過大家熟悉如上的渲染線程再加個RHI與渲染模塊如上這些基本類,應該就能把UE4的源碼都聯系起來了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM