<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 剖析虛幻渲染體系(12)- 移動端專題Part 1(UE移動端渲染分析)

     

     

    12.1 本篇概述

    前面的所有篇章都是基于PC端的延遲渲染管線闡述UE的渲染體系的,特別是剖析虛幻渲染體系(04)- 延遲渲染管線詳盡地闡述了在PC端的延遲渲染管線的流程和步驟。

    此篇只要針對UE的移動端的渲染管線進行闡述,最終還會對比移動和和PC端的渲染差異,以及特殊的優化措施。本篇主要闡述UE渲染體系的以下內容:

    • FMobileSceneRenderer的主要流程和步驟。
    • 移動端的前向和延遲渲染管線。
    • 移動端的光影和陰影。
    • 移動端和PC端的異同,以及涉及的特殊優化技巧。

    特別要指出的是,本篇分析的UE源碼升級到了4.27.1,需要同步看源碼的同學注意更新了。

    如果要在PC的UE編輯器開啟移動端渲染管線,可以選擇如下所示的菜單:

    等待Shader編譯完成,UE編輯器的視口內便是移動端的預覽效果。

    12.1.1 移動設備的特點

    相比PC桌面平臺,移動端在尺寸、電量、硬件性能等諸多方面都存在顯著的差異,具體表現在:

    • 更小的尺寸。移動端的便攜性就要求整機設備必須輕巧,可置于掌中或口袋內,所以整機只能限制在非常小的體積之內。

    • 有限的能量和功率。受限于電池存儲技術,目前的主流鋰電池普通在1萬毫安,但移動設備的分辨率、畫質卻越來越高,為了滿足足夠長的續航和散熱限制,必須嚴格控制移動設備的整機功率,通常在5w以內。

    • 散熱方式受限。PC設備通常可以安裝散熱風扇、甚至水冷系統,而移動設備不具備這些主動散熱方式,只能靠熱傳導散熱。如果散熱不當,CPU和GPU都會主動降頻,以非常有限的性能運行,以免設備元器件因過熱而損毀。

    • 有限的硬件性能。移動設備的各類元件(CPU、帶寬、內存、GPU等)的性能都只是PC設備的數十分之一。

      2018年,主流PC設備(NV GV100-400-A1 Titan V)和主流移動設備(Samsung Exynos 9 8895)的性能對比圖。移動設備的很多硬件性能只是PC設備的幾十分之一,但分辨率卻接近PC的一半,更加突顯了移動設備的挑戰和窘境。

      到了2020年,主流的移動設備性能如下所示:

    • 特殊的硬件架構。如CPU和GPU共享內存存儲設備,被稱為耦合式架構,還有GPU的TB(D)R架構,目的都是為了在低功耗內完成盡可能多的操作。

      PC設備的解耦硬件架構和移動設備的耦合式硬件架構對比圖。

    除此之外,不同于PC端的CPU和GPU純粹地追求計算性能,衡量移動端的性能有三個指標:性能(Performance)、能量(Power)、面積(Area),俗稱PPA。(下圖)

    衡量移動設備的三個基本參數:Performance、Area、Power,其中Compute Density(計算密度)涉及性能和面積,能耗比涉及性能和能力消耗,越大越好。

    與移動設備一起崛起的還有XR設備,它是移動設備的一個重要發展分支。目前存在著各種不同大小、功能、應用場景的XR設備:

    各種形式的XR設備。

    隨著近來元宇宙(Metaverse)的爆火,以及FaceBook改名為Meta,加之Apple、MicroSoft、NVidia、Google等科技巨頭都在加緊布局面向未來的沉浸式體驗,XR設備作為能夠最接近元宇宙暢想的載體和入口,自然成為一條未來非常有潛力能夠出現巨無霸的全新賽道。

     

    12.2 UE移動端渲染特性

    本章闡述一下UE4.27在移動端的渲染特性。

    12.2.1 Feature Level

    UE在移動端支持以下圖形API:

    Feature Level 說明
    OpenGL ES 3.1 安卓系統的默認特性等級,可以在工程設置(Project Settings > Platforms > Android Material Quality - ES31)配置具體的材質參數。
    Android Vulkan 可用于某些特定Android設備的高端渲染器,支持Vulkan 1.2 API,輕量級設計理念的Vulkan,多數情況下會比OpenGL更高效。
    Metal 2.0 專用于iOS設備的特性等級。可以在Project Settings > Platforms > iOS Material Quality配置材質參數。

    在目前的主流安卓設備,使用Vulkan能獲得更好的性能,原因在于Vulkan輕量級的設計理念,使得UE等應用程序能夠更加精準地執行優化。下面是Vulkan和OpenGL的對照表:

    Vulkan OpenGL
    基于對象的狀態,沒有全局狀態。 單一的全局狀態機。
    所有的狀態概念都放置到命令緩沖區中。 狀態被綁定到單個上下文。
    可以多線程編碼。 渲染操作只能被順序執行。
    可以精確、顯式地操控GPU的內存和同步。 GPU的內存和同步細節通常被驅動程序隱藏起來。
    驅動程序沒有運行時錯誤檢測,但存在針對開發人員的驗證層。 廣泛的運行時錯誤檢測。

    如果在Windows平臺,UE編輯器也可以啟動OpenGL、Vulkan、Metal的模擬器,以便在編輯器時預覽效果,但可能跟實際的運行設備的畫面有差異,不可完全依賴此功能。

    開啟Vulkan前需要在工程中配置一些參數,具體參看官方文檔Android Vulkan Mobile Renderer

    另外,UE早前幾個版本就移除了windows下的OpenGL支持,雖然目前UE編輯器還存在OpenGL的模擬選項,但實際上底層是用D3D渲染的。

    12.2.2 Deferred Shading

    UE的Deferred Shading(延遲著色)是在4.26才加入的功能,使得開發者能夠在移動端實現較復雜的光影效果,諸如高質量反射、多動態光照、貼花、高級光照特性。

    上:前向渲染;下:延遲渲染。

    如果要在移動端開啟延遲渲染,需要在工程配置目錄下的DefaultEngine.ini添加r.Mobile.ShadingPath=1字段,然后重啟編輯器。

    12.2.3 Ground Truth Ambient Occlusion

    Ground Truth Ambient Occlusion (GTAO)是接近現實世界的環境遮擋技術,是陰影的一種補償,能夠遮蔽一部分非直接光照,從而獲得良好的軟陰影效果。

    開啟了GTAO的效果,注意機器人靠近墻面時,會在墻面留下具有漸變的軟陰影效果。

    為了開啟GTAO,需要勾選以下所示的選項:

    此外,GTAO依賴Mobile HDR的選項,為了在對應目標設備開啟,還需要在[Platform]Scalability.ini的配置中添加r.Mobile.AmbientOcclusionQuality字段,并且值需要大于0,否則GTAO將被禁用。

    值得注意的是,GTAO在Mali設備上存在性能問題,因為它們的最大Compute Shader線程數量少于1024個。

    12.2.4 Dynamic Lighting and Shadow

    UE在移動端實現的光源特性有:

    • 線性空間的HDR光照。
    • 帶方向的光照圖(考慮了法線)。
    • 太陽(平行光)支持距離場陰影 + 解析的鏡面高光。
    • IBL光照:每個物體采樣了最近的一個反射捕捉器,而沒有視差校正。
    • 動態物體能正確地接受光照,也可以投射陰影。

    UE移動端支持的動態光源的類型、數量、陰影等信息如下:

    光源類型 最大數量 陰影 描述
    平行光 1 CSM CSM默認是2級,最多支持4級。
    點光源 4 不支持 點光源陰影需要立方體陰影圖,而單Pass渲染立方體陰影(OnePassPointLightShadow)的技術需要GS(SM5才有)才支持。
    聚光燈 4 支持 默認禁用,需要在工程中開啟。
    區域光 0 不支持 目前不支持動態區域光照效果。

    動態的聚光燈需要在工程配置中顯式開啟:

    在移動BasePass的像素著色器中,聚光燈陰影圖與CSM共享相同的紋理采樣器,并且聚光燈陰影和CSM使用相同的陰影圖圖集。CSM能夠保證有足夠的空間,而聚光燈將按陰影分辨率排序。

    默認情況下,可見陰影的最大數量被限制為8個,但可以通過改變r.Mobile.MaxVisibleMovableSpotLightsShadow的值來改變上限值。聚光燈陰影的分辨率是基于屏幕大小和r.Shadow.TexelsPerPixelSpotlight

    在前向渲染路徑中,局部光源(點光源和聚光燈)的總數不能超過4個。

    移動端還支持一種特殊的陰影模式,那就是調制陰影(Modulated Shadows),只能用于固定(Stationary)的平行光。開啟了調制陰影的效果圖如下:

    調制陰影還支持改變陰影顏色和混合比例:

    左:動態陰影;右:調制陰影。

    移動端的陰影同樣支持自陰影、陰影質量等級(r.shadowquality)、深度偏移等參數的設置。

    此外,移動端默認使用了GGX的高光反射,如果想切換到傳統的高光著色模型,可以在以下配置里修改:

    12.2.5 Pixel Projected Reflection

    UE針對移動端做了一個優化版的SSR,被稱為Pixel Projected Reflection(PPR),也是復用屏幕空間像素的核心思想。

    PPR效果圖。

    為了開啟PPR效果,需要滿足以下條件:

    • 開啟MobileHDR選項。

    • r.Mobile.PixelProjectedReflectionQuality的值大于0。

    • 設置Project Settings > Mobile and set the Planar Reflection Mode成正確的模式:

      Planar Reflection Mode有3個選項:

      • Usual:平面反射Actor在所有平臺上的作用都是相同的。
      • MobilePPR:平面反射Actor在PC/主機平臺上正常工作,但在移動平臺上使用PPR渲染。
      • MobilePPRExclusive:平面反射Actor將只用于移動平臺上的PPR,為PC和Console項目留下了使用傳統SSR的空間。

    默認只有高端移動設備才會在[Project]Scalability.ini開啟r.Mobile.PixelProjectedReflectionQuality

    12.2.6 Mesh Auto-Instancing

    PC端的網格繪制管線已經支持了網格的自動化實例和合并繪制,這個特性可以極大提升渲染性能。4.27已經在移動端支持了這一特性。

    若想開啟,則需要打開工程配置目錄下的DefaultEngine.ini,添加以下字段:

    r.Mobile.SupportGPUScene=1
    r.Mobile.UseGPUSceneTexture=1
    

    重啟編輯器,等待Shader編譯完即可預覽效果。

    由于需要GPUSceneTexture支持,而Mali設備的Uniform Buffer最大只有64kb,以致無法支持足夠大的空間,所以,Mali設備會使用紋理而非緩沖區來存儲GPUScene數據。

    但也存在一些限制:

    • 移動設備上的自動實例化主要有利于CPU密集型項目,而不是GPU密集型項目。雖然啟用自動實例化不太可能會對GPU密集型的項目造成損害,但不太可能看到使用它帶來的顯著性能改進。

    • 如果一款游戲或應用需要大量內存,那么關閉r.Mobile.UseGPUSceneTexture并使用緩沖區可能會更有好處,因為它無法在Mali設備上正常運行。

      也可以針對Mali設備關閉r.Mobile.UseGPUSceneTexture,而其它GPU廠商的設備正常使用。

    自動實例化的有效性很大程度上取決于項目的確切規范和定位,建議創建一個啟用了自動實例化的構建,并對其進行概要分析,以確定是否會看到實質性的性能提升。

    12.2.7 Post Processing

    由于移動設備存在更慢的依賴紋理讀取(dependent texture read)、有限的硬件特性、特殊的硬件架構、額外的渲染目標解析、有限的帶寬等限制性因素,故而后處理在移動設備上執行起來會比較耗性能,有些極端情況會卡住渲染管線。

    盡管如此,在某些畫質要求高的游戲或應用,依然非常依賴后處理的強勁表現力,為高品質邁上幾個臺階。UE不會限制開發者使用后處理。

    為了開啟后處理,必須先開啟MobileHDR選項:

    開啟后處理之后,就可以在后處理體積(Post Process Volume)設置各種后處理效果。

    在移動端可以支持的后處理有Mobile Tonemapper、Color Grading、Lens、Bloom、Dirt Mask、Auto Exposure、Lens Flares、Depth of Field等等。

    為了獲得更好的性能,官方給出的建議是在移動端只開啟Bloom和TAA。

    12.2.8 其它特性和限制

    • Reflection Capture Compression

    移動端支持反射捕捉器組件(Reflection Capture Component)的壓縮,可以減少Reflection Capture運行時的內存和帶寬,提升渲染效率。需要在工程配置中開啟:

    開啟之后,默認使用ETC2進行壓縮。另外,也可以針對每個Reflection Capture Component進行調整:

    • 材質特性

    移動平臺上的材質(特性級別Open ES 3.1)使用與其他平臺相同的基于節點的創建過程,并且絕大多數節點在移動端都支持。

    移動平臺支持的材質屬性有:BaseColorRoughnessMetallicSpecularNormalEmissiveRefraction,但不支持Scene Color表達式、Tessellation輸入、次表面散射著色模型。

    移動平臺支持的材質存在一些限制:

    • 由于硬件限制,只能使用16個紋理采樣器。
    • 只有DefaultLit和Unlit著色模型可用。
    • 自定義UV應該用來避免依賴紋理讀取(沒有紋理uv的數學計算)。
    • 半透明和Masked材質是及其耗性能, 建議盡量使用不透明材質。
    • 深度淡出(Depth fade)可以在iOS平臺的半透明材質中使用,但在硬件不支持從深度緩沖區獲取數據的平臺上,是不受支持的,將導致不可接受的性能成本。

    材質屬性面板存在一些針對移動端的特殊選項:

    這些屬性的說明如下:

    • Mobile Separate Translucency:是否在移動端開啟單獨的半透明渲染紋理。

    • Use Full Precision:是否使用全精度,如果否,可以減少帶寬占用和能耗,提升性能,但可能會出現遠處物體的瑕疵:

      左:全精度材質;右:半精度材質,遠處的太陽出現了瑕疵。

    • Use Lightmap Directionality:是否開啟光照圖的方向性,若勾選,會考慮光照圖的方向和像素法線,但會提升性能消耗。

    • Use Alpha to Coverage:是否為Masked材質開啟MSAA抗鋸齒,若勾選,會開啟MSAA。

    • Fully Rough:是否完全粗糙,如果勾選,將極大提升此材質的渲染效率。

    此外,移動端支持的網格類型有:

    • Skeletal Mesh
    • Static Mesh
    • Landscape
    • CPU particle sprites, particle meshes

    除上述類型以外的其它都不被支持。其它限制還有:

    • 單個網格最多只能到65k,因為頂點索引只有16位。
    • 單個Skeletal Mesh的骨骼數量必須在75個以內,因為受硬件性能的限制。

     

    12.3 FMobileSceneRenderer

    FMobileSceneRenderer繼承自FSceneRenderer,它負責移動端的場景渲染流程,而PC端是同樣繼承自FSceneRenderer的FDeferredShadingSceneRenderer。它們的繼承關系圖如下:

    classDiagram-v2 FSceneRenderer <|-- FMobileSceneRenderer FSceneRenderer <|-- FDeferredShadingSceneRenderer

    前述多篇文章已經提及了FDeferredShadingSceneRenderer,它的渲染流程尤為復雜,包含了復雜的光影和渲染步驟。相比之下,FMobileSceneRenderer的邏輯和步驟會簡單許多,下面是RenderDoc的截幀:

    以上主要包含了InitViews、ShadowDepths、PrePass、BasePass、OcclusionTest、ShadowProjectionOnOpaque、Translucency、PostProcessing等步驟。其中這些步驟在PC端都是存在的,但實現過程可能會有所不同。見后續章節剖析。

    12.3.1 渲染器主流程

    移動端的場景渲染器的主流程也發生在FMobileSceneRenderer::Render中,代碼和解析如下:

    // Engine\Source\Runtime\Renderer\Private\MobileShadingRenderer.cpp
    
    void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
    {
        // 更新圖元場景信息。
        Scene->UpdateAllPrimitiveSceneInfos(RHICmdList);
    
        // 準備視圖的渲染區域.
        PrepareViewRectsForRendering(RHICmdList);
    
        // 準備天空大氣的數據
        if (ShouldRenderSkyAtmosphere(Scene, ViewFamily.EngineShowFlags))
        {
            for (int32 LightIndex = 0; LightIndex < NUM_ATMOSPHERE_LIGHTS; ++LightIndex)
            {
                if (Scene->AtmosphereLights[LightIndex])
                {
                    PrepareSunLightProxy(*Scene->GetSkyAtmosphereSceneInfo(), LightIndex, *Scene->AtmosphereLights[LightIndex]);
                }
            }
        }
        else
        {
            Scene->ResetAtmosphereLightsProperties();
        }
    
        if(!ViewFamily.EngineShowFlags.Rendering)
        {
            return;
        }
    
        // 等待遮擋剔除測試.
        WaitOcclusionTests(RHICmdList);
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    
        // 初始化視圖, 查找可見圖元, 準備渲染所需的RT和緩沖區等數據.
        InitViews(RHICmdList);
        
        if (GRHINeedsExtraDeletionLatency || !GRHICommandList.Bypass())
        {
            QUICK_SCOPE_CYCLE_COUNTER(STAT_FMobileSceneRenderer_PostInitViewsFlushDel);
    
            // 可能會暫停遮擋查詢,所以最好在等待時讓RHI線程和GPU工作. 此外,當執行RHI線程時,這是唯一將處理掛起刪除的位置.
            FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
            FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::FlushRHIThreadFlushResources);
        }
    
        GEngine->GetPreRenderDelegate().Broadcast();
    
        // 在渲染開始前提交全局動態緩沖.
        DynamicIndexBuffer.Commit();
        DynamicVertexBuffer.Commit();
        DynamicReadBuffer.Commit();
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_SceneSim));
    
        if (ViewFamily.bLateLatchingEnabled)
        {
            BeginLateLatching(RHICmdList);
        }
    
        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
    
        // 處理虛擬紋理
        if (bUseVirtualTexturing)
        {
            SCOPED_GPU_STAT(RHICmdList, VirtualTextureUpdate);
            
            FVirtualTextureSystem::Get().Update(RHICmdList, FeatureLevel, Scene);
            
            // Clear virtual texture feedback to default value
            FUnorderedAccessViewRHIRef FeedbackUAV = SceneContext.GetVirtualTextureFeedbackUAV();
            RHICmdList.Transition(FRHITransitionInfo(FeedbackUAV, ERHIAccess::SRVMask, ERHIAccess::UAVMask));
            RHICmdList.ClearUAVUint(FeedbackUAV, FUintVector4(~0u, ~0u, ~0u, ~0u));
            RHICmdList.Transition(FRHITransitionInfo(FeedbackUAV, ERHIAccess::UAVMask, ERHIAccess::UAVMask));
            RHICmdList.BeginUAVOverlap(FeedbackUAV);
        }
        
        // 已排序的光源信息.
        FSortedLightSetSceneInfo SortedLightSet;
        
        // 延遲渲染.
        if (bDeferredShading)
        {
            // 收集和排序光源.
            GatherAndSortLights(SortedLightSet);
            
            int32 NumReflectionCaptures = Views[0].NumBoxReflectionCaptures + Views[0].NumSphereReflectionCaptures;
            bool bCullLightsToGrid = (NumReflectionCaptures > 0 || GMobileUseClusteredDeferredShading != 0);
            FRDGBuilder GraphBuilder(RHICmdList);
            // 計算光源格子.
            ComputeLightGrid(GraphBuilder, bCullLightsToGrid, SortedLightSet);
            GraphBuilder.Execute();
        }
    
        // 生成天空/大氣LUT.
        const bool bShouldRenderSkyAtmosphere = ShouldRenderSkyAtmosphere(Scene, ViewFamily.EngineShowFlags);
        if (bShouldRenderSkyAtmosphere)
        {
            FRDGBuilder GraphBuilder(RHICmdList);
            RenderSkyAtmosphereLookUpTables(GraphBuilder);
            GraphBuilder.Execute();
        }
    
        // 通知特效系統場景準備渲染.
        if (FXSystem && ViewFamily.EngineShowFlags.Particles)
        {
            FXSystem->PreRender(RHICmdList, NULL, !Views[0].bIsPlanarReflection);
            if (FGPUSortManager* GPUSortManager = FXSystem->GetGPUSortManager())
            {
                GPUSortManager->OnPreRender(RHICmdList);
            }
        }
        // 輪詢遮擋剔除請求.
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Shadows));
    
        // 渲染陰影.
        RenderShadowDepthMaps(RHICmdList);
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    
        // 收集視圖列表.
        TArray<const FViewInfo*> ViewList;
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++) 
        {
            ViewList.Add(&Views[ViewIndex]);
        }
    
        // 渲染自定義深度.
        if (bShouldRenderCustomDepth)
        {
            FRDGBuilder GraphBuilder(RHICmdList);
            FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(GraphBuilder, Views[0].GetFeatureLevel(), ESceneTextureSetupMode::None);
            RenderCustomDepthPass(GraphBuilder, SceneTextures);
            GraphBuilder.Execute();
        }
    
        // 渲染深度PrePass.
        if (bIsFullPrepassEnabled)
        {
            // SDF和AO需要完整的PrePass深度.
    
            FRHIRenderPassInfo DepthPrePassRenderPassInfo(
                SceneContext.GetSceneDepthSurface(),
                EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil);
    
            DepthPrePassRenderPassInfo.NumOcclusionQueries = ComputeNumOcclusionQueriesToBatch();
            DepthPrePassRenderPassInfo.bOcclusionQueries = DepthPrePassRenderPassInfo.NumOcclusionQueries != 0;
    
            RHICmdList.BeginRenderPass(DepthPrePassRenderPassInfo, TEXT("DepthPrepass"));
    
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
            
            // 渲染完整的深度PrePass.
            RenderPrePass(RHICmdList);
    
            // 提交遮擋剔除.
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
            RenderOcclusion(RHICmdList);
    
            RHICmdList.EndRenderPass();
    
            // SDF陰影
            if (bRequiresDistanceFieldShadowingPass)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderSDFShadowing);
                RenderSDFShadowing(RHICmdList);
            }
    
            // HZB.
            if (bShouldRenderHZB)
            {
                RenderHZB(RHICmdList, SceneContext.SceneDepthZ);
            }
    
            // AO.
            if (bRequiresAmbientOcclusionPass)
            {
                RenderAmbientOcclusion(RHICmdList, SceneContext.SceneDepthZ);
            }
        }
    
        FRHITexture* SceneColor = nullptr;
        
        // 延遲渲染.
        if (bDeferredShading)
        {
            SceneColor = RenderDeferred(RHICmdList, ViewList, SortedLightSet);
        }
        // 前向渲染.
        else
        {
            SceneColor = RenderForward(RHICmdList, ViewList);
        }
        
        // 渲染速度緩沖.
        if (bShouldRenderVelocities)
        {
            FRDGBuilder GraphBuilder(RHICmdList);
    
            FRDGTextureMSAA SceneDepthTexture = RegisterExternalTextureMSAA(GraphBuilder, SceneContext.SceneDepthZ);
            FRDGTextureRef VelocityTexture = TryRegisterExternalTexture(GraphBuilder, SceneContext.SceneVelocity);
    
            if (VelocityTexture != nullptr)
            {
                AddClearRenderTargetPass(GraphBuilder, VelocityTexture);
            }
    
            // 渲染可移動物體的速度緩沖.
            AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_Velocity));
            RenderVelocities(GraphBuilder, SceneDepthTexture.Resolve, VelocityTexture, FSceneTextureShaderParameters(), EVelocityPass::Opaque, false);
            AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_AfterVelocity));
    
            // 渲染透明物體的速度緩沖.
            AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_TranslucentVelocity));
            RenderVelocities(GraphBuilder, SceneDepthTexture.Resolve, VelocityTexture, GetSceneTextureShaderParameters(CreateMobileSceneTextureUniformBuffer(GraphBuilder, EMobileSceneTextureSetupMode::SceneColor)), EVelocityPass::Translucent, false);
    
            GraphBuilder.Execute();
        }
    
        // 處理場景渲染后的邏輯.
        {
            FRendererModule& RendererModule = static_cast<FRendererModule&>(GetRendererModule());
            FRDGBuilder GraphBuilder(RHICmdList);
            RendererModule.RenderPostOpaqueExtensions(GraphBuilder, Views, SceneContext);
    
            if (FXSystem && Views.IsValidIndex(0))
            {
                AddUntrackedAccessPass(GraphBuilder, [this](FRHICommandListImmediate& RHICmdList)
                {
                    check(RHICmdList.IsOutsideRenderPass());
    
                    FXSystem->PostRenderOpaque(
                        RHICmdList,
                        Views[0].ViewUniformBuffer,
                        nullptr,
                        nullptr,
                        Views[0].AllowGPUParticleUpdate()
                    );
                    if (FGPUSortManager* GPUSortManager = FXSystem->GetGPUSortManager())
                    {
                        GPUSortManager->OnPostRenderOpaque(RHICmdList);
                    }
                });
            }
            GraphBuilder.Execute();
        }
    
        // 刷新/提交命令緩沖.
        if (bSubmitOffscreenRendering)
        {
            RHICmdList.SubmitCommandsHint();
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
        
        // 轉換場景顏色成SRV, 以供后續步驟讀取.
        if (!bGammaSpace || bRenderToSceneColor)
        {
            RHICmdList.Transition(FRHITransitionInfo(SceneColor, ERHIAccess::Unknown, ERHIAccess::SRVMask));
        }
    
        if (bDeferredShading)
        {
            // 釋放場景渲染目標上的原始引用.
            SceneContext.AdjustGBufferRefCount(RHICmdList, -1);
        }
    
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Post));
    
        // 處理虛擬紋理.
        if (bUseVirtualTexturing)
        {    
            SCOPED_GPU_STAT(RHICmdList, VirtualTextureUpdate);
    
            // No pass after this should make VT page requests
            RHICmdList.EndUAVOverlap(SceneContext.VirtualTextureFeedbackUAV);
            RHICmdList.Transition(FRHITransitionInfo(SceneContext.VirtualTextureFeedbackUAV, ERHIAccess::UAVMask, ERHIAccess::SRVMask));
    
            TArray<FIntRect, TInlineAllocator<4>> ViewRects;
            ViewRects.AddUninitialized(Views.Num());
            for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
            {
                ViewRects[ViewIndex] = Views[ViewIndex].ViewRect;
            }
            
            FVirtualTextureFeedbackBufferDesc Desc;
            Desc.Init2D(SceneContext.GetBufferSizeXY(), ViewRects, SceneContext.GetVirtualTextureFeedbackScale());
    
            SubmitVirtualTextureFeedbackBuffer(RHICmdList, SceneContext.VirtualTextureFeedback, Desc);
        }
    
        FMemMark Mark(FMemStack::Get());
        FRDGBuilder GraphBuilder(RHICmdList);
    
        FRDGTextureRef ViewFamilyTexture = TryCreateViewFamilyTexture(GraphBuilder, ViewFamily);
        
        // 解析場景
        if (ViewFamily.bResolveScene)
        {
            if (!bGammaSpace || bRenderToSceneColor)
            {
                // 完成每個視圖的渲染或完整的立體聲緩沖區(如果啟用)
                {
                    RDG_EVENT_SCOPE(GraphBuilder, "PostProcessing");
                    SCOPE_CYCLE_COUNTER(STAT_FinishRenderViewTargetTime);
    
                    TArray<TRDGUniformBufferRef<FMobileSceneTextureUniformParameters>, TInlineAllocator<1, SceneRenderingAllocator>> MobileSceneTexturesPerView;
                    MobileSceneTexturesPerView.SetNumZeroed(Views.Num());
    
                    const auto SetupMobileSceneTexturesPerView = [&]()
                    {
                        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
                        {
                            EMobileSceneTextureSetupMode SetupMode = EMobileSceneTextureSetupMode::SceneColor;
                            if (Views[ViewIndex].bCustomDepthStencilValid)
                            {
                                SetupMode |= EMobileSceneTextureSetupMode::CustomDepth;
                            }
    
                            if (bShouldRenderVelocities)
                            {
                                SetupMode |= EMobileSceneTextureSetupMode::SceneVelocity;
                            }
    
                            MobileSceneTexturesPerView[ViewIndex] = CreateMobileSceneTextureUniformBuffer(GraphBuilder, SetupMode);
                        }
                    };
    
                    SetupMobileSceneTexturesPerView();
    
                    FMobilePostProcessingInputs PostProcessingInputs;
                    PostProcessingInputs.ViewFamilyTexture = ViewFamilyTexture;
    
                    // 渲染后處理效果.
                    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
                    {
                        RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, Views.Num() > 1, "View%d", ViewIndex);
                        PostProcessingInputs.SceneTextures = MobileSceneTexturesPerView[ViewIndex];
                        AddMobilePostProcessingPasses(GraphBuilder, Views[ViewIndex], PostProcessingInputs, NumMSAASamples > 1);
                    }
                }
            }
        }
    
        GEngine->GetPostRenderDelegate().Broadcast();
    
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_SceneEnd));
    
        if (bShouldRenderVelocities)
        {
            SceneContext.SceneVelocity.SafeRelease();
        }
        
        if (ViewFamily.bLateLatchingEnabled)
        {
            EndLateLatching(RHICmdList, Views[0]);
        }
    
        RenderFinish(GraphBuilder, ViewFamilyTexture);
        GraphBuilder.Execute();
    
        // 輪詢遮擋剔除請求.
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    }
    

    看過剖析虛幻渲染體系(04)- 延遲渲染管線篇章的同學應該都知道,移動端的場景渲染過程精簡了很多很多步驟,相當于是PC端場景渲染器的一個子集。當然,為了適應移動端特有的GPU硬件架構,移動端的場景渲染也有區別于PC端的地方。后面會詳細剖析。移動端場景的主要步驟和流程如下所示:

    stateDiagram-v2 state bDeferredShading <<choice>> state bIsFullPrepassEnabled <<choice>> [*] --> UpdateAllPrimitiveSceneInfos UpdateAllPrimitiveSceneInfos --> PrepareViewRectsForRendering PrepareViewRectsForRendering --> InitViews InitViews --> bDeferredShading bDeferredShading --> RenderSkyAtmosphereLookUpTables* : No bDeferredShading --> GatherAndSortLights: Yes GatherAndSortLights --> ComputeLightGrid ComputeLightGrid --> RenderSkyAtmosphereLookUpTables* RenderSkyAtmosphereLookUpTables* --> RenderShadowDepthMaps RenderShadowDepthMaps --> RenderCustomDepthPass* RenderCustomDepthPass* --> bIsFullPrepassEnabled bIsFullPrepassEnabled --> bDeferredShading2: No bIsFullPrepassEnabled --> RenderPrePass:Yes RenderPrePass --> RenderOcclusion RenderOcclusion --> RenderSDFShadowing* RenderSDFShadowing* --> RenderHZB* RenderHZB* --> RenderAmbientOcclusion* RenderAmbientOcclusion* --> bDeferredShading2 bDeferredShading2-->RenderForward:No bDeferredShading2-->RenderDeferred:Yes RenderDeferred --> RenderVelocities* RenderForward --> RenderVelocities* RenderVelocities* --> AddMobilePostProcessingPasses* AddMobilePostProcessingPasses*-->RenderFinish RenderFinish --> [*]

    關于上面的流程圖,有以下幾點需要加以說明:

    • 流程圖節點bDeferredShadingbDeferredShading2是同一個變量,這里區分開主要是為了防止mermaid語法繪圖錯誤。
    • 帶*的節點是有條件的,非必然執行的步驟。

    UE4.26便加入了移動端的延遲渲染管線,所以上述代碼中有前向渲染分支RenderForward和延遲渲染分支RenderDeferred,它們返回的都是渲染結果SceneColor。

    移動端也支持了圖元GPU場景、SDF陰影、AO、天空大氣、虛擬紋理、遮擋剔除等渲染特性。

    自UE4.26開始,渲染體系廣泛地使用了RDG系統,移動端的場景渲染器也不例外。上述代碼中總共聲明了數個FRDGBuilder實例,用于計算光源格子,以及渲染天空大氣LUT、自定義深度、速度緩沖、渲染后置事件、后處理等,它們都是相對獨立的功能模塊或渲染階段。

    12.3.2 RenderForward

    RenderForward在移動端場景渲染器中負責前向渲染的分支,它的代碼和解析如下:

    FRHITexture* FMobileSceneRenderer::RenderForward(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> ViewList)
    {
        const FViewInfo& View = *ViewList[0];
        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
                    
        FRHITexture* SceneColor = nullptr;
        FRHITexture* SceneColorResolve = nullptr;
        FRHITexture* SceneDepth = nullptr;
        ERenderTargetActions ColorTargetAction = ERenderTargetActions::Clear_Store;
        EDepthStencilTargetActions DepthTargetAction = EDepthStencilTargetActions::ClearDepthStencil_DontStoreDepthStencil;
    
        // 是否啟用移動端MSAA.
        bool bMobileMSAA = NumMSAASamples > 1 && SceneContext.GetSceneColorSurface()->GetNumSamples() > 1;
    
        // 是否啟用移動端多試圖模式.
        static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView"));
        const bool bIsMultiViewApplication = (CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0);
        
        // gamma空間的渲染分支.
        if (bGammaSpace && !bRenderToSceneColor)
        {
            // 如果開啟MSAA, 則從SceneContext獲取渲染紋理(包含場景顏色和解析紋理)
            if (bMobileMSAA)
            {
                SceneColor = SceneContext.GetSceneColorSurface();
                SceneColorResolve = ViewFamily.RenderTarget->GetRenderTargetTexture();
                ColorTargetAction = ERenderTargetActions::Clear_Resolve;
                RHICmdList.Transition(FRHITransitionInfo(SceneColorResolve, ERHIAccess::Unknown, ERHIAccess::RTV | ERHIAccess::ResolveDst));
            }
            // 非MSAA,從視圖家族獲取渲染紋理.
            else
            {
                SceneColor = ViewFamily.RenderTarget->GetRenderTargetTexture();
                RHICmdList.Transition(FRHITransitionInfo(SceneColor, ERHIAccess::Unknown, ERHIAccess::RTV));
            }
            SceneDepth = SceneContext.GetSceneDepthSurface();
        }
        // 線性空間或渲染到場景紋理.
        else
        {
            SceneColor = SceneContext.GetSceneColorSurface();
            if (bMobileMSAA)
            {
                SceneColorResolve = SceneContext.GetSceneColorTexture();
                ColorTargetAction = ERenderTargetActions::Clear_Resolve;
                RHICmdList.Transition(FRHITransitionInfo(SceneColorResolve, ERHIAccess::Unknown, ERHIAccess::RTV | ERHIAccess::ResolveDst));
            }
            else
            {
                SceneColorResolve = nullptr;
                ColorTargetAction = ERenderTargetActions::Clear_Store;
            }
    
            SceneDepth = SceneContext.GetSceneDepthSurface();
                    
            if (bRequiresMultiPass)
            {    
                // store targets after opaque so translucency render pass can be restarted
                ColorTargetAction = ERenderTargetActions::Clear_Store;
                DepthTargetAction = EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil;
            }
                            
            if (bKeepDepthContent)
            {
                // store depth if post-processing/capture needs it
                DepthTargetAction = EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil;
            }
        }
    
        // prepass的深度紋理狀態.
        if (bIsFullPrepassEnabled)
        {
            ERenderTargetActions DepthTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetDepthActions(DepthTargetAction)));
            ERenderTargetActions StencilTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetStencilActions(DepthTargetAction)));
            DepthTargetAction = MakeDepthStencilTargetActions(DepthTarget, StencilTarget);
        }
    
        FRHITexture* ShadingRateTexture = nullptr;
        
        if (!View.bIsSceneCapture && !View.bIsReflectionCapture)
        {
            TRefCountPtr<IPooledRenderTarget> ShadingRateTarget = GVRSImageManager.GetMobileVariableRateShadingImage(ViewFamily);
            if (ShadingRateTarget.IsValid())
            {
                ShadingRateTexture = ShadingRateTarget->GetRenderTargetItem().ShaderResourceTexture;
            }
        }
    
        // 場景顏色渲染Pass信息.
        FRHIRenderPassInfo SceneColorRenderPassInfo(
            SceneColor,
            ColorTargetAction,
            SceneColorResolve,
            SceneDepth,
            DepthTargetAction,
            nullptr, // we never resolve scene depth on mobile
            ShadingRateTexture,
            VRSRB_Sum,
            FExclusiveDepthStencil::DepthWrite_StencilWrite
        );
        SceneColorRenderPassInfo.SubpassHint = ESubpassHint::DepthReadSubpass;
        if (!bIsFullPrepassEnabled)
        {
            SceneColorRenderPassInfo.NumOcclusionQueries = ComputeNumOcclusionQueriesToBatch();
            SceneColorRenderPassInfo.bOcclusionQueries = SceneColorRenderPassInfo.NumOcclusionQueries != 0;
        }
        // 如果場景顏色不是多視圖,但應用程序是,需要渲染為單視圖的多視圖給著色器.
        SceneColorRenderPassInfo.MultiViewCount = View.bIsMobileMultiViewEnabled ? 2 : (bIsMultiViewApplication ? 1 : 0);
    
        // 開始渲染場景顏色.
        RHICmdList.BeginRenderPass(SceneColorRenderPassInfo, TEXT("SceneColorRendering"));
        
        if (GIsEditor && !View.bIsSceneCapture)
        {
            DrawClearQuad(RHICmdList, Views[0].BackgroundColor);
        }
    
        if (!bIsFullPrepassEnabled)
        {
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
            // 渲染深度pre-pass
            RenderPrePass(RHICmdList);
        }
    
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Opaque));
        // 渲染BasePass: 不透明和masked物體.
        RenderMobileBasePass(RHICmdList, ViewList);
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    
        //渲染調試模式.
    #if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
        if (ViewFamily.UseDebugViewPS())
        {
            // Here we use the base pass depth result to get z culling for opaque and masque.
            // The color needs to be cleared at this point since shader complexity renders in additive.
            DrawClearQuad(RHICmdList, FLinearColor::Black);
            RenderMobileDebugView(RHICmdList, ViewList);
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
    #endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
    
        const bool bAdrenoOcclusionMode = CVarMobileAdrenoOcclusionMode.GetValueOnRenderThread() != 0;
        if (!bIsFullPrepassEnabled)
        {
            // 遮擋剔除
            if (!bAdrenoOcclusionMode)
            {
                // 提交遮擋剔除
                RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
                RenderOcclusion(RHICmdList);
                RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
            }
        }
    
        // 后置事件, 處理插件渲染.
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(ViewExtensionPostRenderBasePass);
            QUICK_SCOPE_CYCLE_COUNTER(STAT_FMobileSceneRenderer_ViewExtensionPostRenderBasePass);
            for (int32 ViewExt = 0; ViewExt < ViewFamily.ViewExtensions.Num(); ++ViewExt)
            {
                for (int32 ViewIndex = 0; ViewIndex < ViewFamily.Views.Num(); ++ViewIndex)
                {
                    ViewFamily.ViewExtensions[ViewExt]->PostRenderBasePass_RenderThread(RHICmdList, Views[ViewIndex]);
                }
            }
        }
        
        // 如果需要渲染透明物體或像素投影的反射, 則需要拆分pass.
        if (bRequiresMultiPass || bRequiresPixelProjectedPlanarRelfectionPass)
        {
            RHICmdList.EndRenderPass();
        }
           
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Translucency));
    
        // 如果需要, 則重新開啟透明渲染通道.
        if (bRequiresMultiPass || bRequiresPixelProjectedPlanarRelfectionPass)
        {
            check(RHICmdList.IsOutsideRenderPass());
    
            // 如果當前硬件不支持讀寫相同的深度緩沖區,則復制場景深度.
            ConditionalResolveSceneDepth(RHICmdList, View);
    
            if (bRequiresPixelProjectedPlanarRelfectionPass)
            {
                const FPlanarReflectionSceneProxy* PlanarReflectionSceneProxy = Scene ? Scene->GetForwardPassGlobalPlanarReflection() : nullptr;
                RenderPixelProjectedReflection(RHICmdList, SceneContext, PlanarReflectionSceneProxy);
    
                FRHITransitionInfo TranslucentRenderPassTransitions[] = {
                FRHITransitionInfo(SceneColor, ERHIAccess::SRVMask, ERHIAccess::RTV),
                FRHITransitionInfo(SceneDepth, ERHIAccess::SRVMask, ERHIAccess::DSVWrite)
                };
                RHICmdList.Transition(MakeArrayView(TranslucentRenderPassTransitions, UE_ARRAY_COUNT(TranslucentRenderPassTransitions)));
            }
    
            DepthTargetAction = EDepthStencilTargetActions::LoadDepthStencil_DontStoreDepthStencil;
            FExclusiveDepthStencil::Type ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilRead;
            if (bModulatedShadowsInUse)
            {
                ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilWrite;
            }
    
            // 用于移動端像素投影反射的不透明網格必須將深度寫入深度RT, 因為只渲染一次網格(如果質量水平低于或等于BestPerformance).
            if (IsMobilePixelProjectedReflectionEnabled(View.GetShaderPlatform())
                && GetMobilePixelProjectedReflectionQuality() == EMobilePixelProjectedReflectionQuality::BestPerformance)
            {
                ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;
            }
    
            if (bKeepDepthContent && !bMobileMSAA)
            {
                DepthTargetAction = EDepthStencilTargetActions::LoadDepthStencil_StoreDepthStencil;
            }
    
    #if PLATFORM_HOLOLENS
            if (bShouldRenderDepthToTranslucency)
            {
                ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;
            }
    #endif
    
            // 透明物體渲染Pass.
            FRHIRenderPassInfo TranslucentRenderPassInfo(
                SceneColor,
                SceneColorResolve ? ERenderTargetActions::Load_Resolve : ERenderTargetActions::Load_Store,
                SceneColorResolve,
                SceneDepth,
                DepthTargetAction, 
                nullptr,
                ShadingRateTexture,
                VRSRB_Sum,
                ExclusiveDepthStencil
            );
            TranslucentRenderPassInfo.NumOcclusionQueries = 0;
            TranslucentRenderPassInfo.bOcclusionQueries = false;
            TranslucentRenderPassInfo.SubpassHint = ESubpassHint::DepthReadSubpass;
            
            // 開始渲染半透明物體.
            RHICmdList.BeginRenderPass(TranslucentRenderPassInfo, TEXT("SceneColorTranslucencyRendering"));
        }
    
        // 場景深度是只讀的,可以獲取.
        RHICmdList.NextSubpass();
    
        if (!View.bIsPlanarReflection)
        {
            // 渲染貼花.
            if (ViewFamily.EngineShowFlags.Decals)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
                RenderDecals(RHICmdList);
            }
    
            // 渲染調制陰影投射.
            if (ViewFamily.EngineShowFlags.DynamicShadows)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderShadowProjections);
                RenderModulatedShadowProjections(RHICmdList);
            }
        }
        
        // 繪制半透明.
        if (ViewFamily.EngineShowFlags.Translucency)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderTranslucency);
            SCOPE_CYCLE_COUNTER(STAT_TranslucencyDrawTime);
            
            RenderTranslucency(RHICmdList, ViewList);
            
            FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
    
        if (!bIsFullPrepassEnabled)
        {
            // Adreno遮擋剔除模式.
            if (bAdrenoOcclusionMode)
            {
                RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
                // flush
                RHICmdList.SubmitCommandsHint();
                bSubmitOffscreenRendering = false; // submit once
                // Issue occlusion queries
                RenderOcclusion(RHICmdList);
                RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
            }
        }
    
        // 在MSAA被解析前預計算色調映射(只在iOS有效)
        if (!bGammaSpace)
        {
            PreTonemapMSAA(RHICmdList);
        }
    
        // 結束場景顏色渲染.
        RHICmdList.EndRenderPass();
    
        // 優化返回場景顏色的解析紋理(開啟了MSAA才有).
        return SceneColorResolve ? SceneColorResolve : SceneColor;
    }
    

    移動端前向渲染主要步驟跟PC端類似,依次渲染PrePass、BasePass、特殊渲染(貼花、AO、遮擋剔除等)、半透明物體。它們的流程圖如下:

    stateDiagram-v2 [*] --> DrawClearQuad* DrawClearQuad* --> RenderPrePass* RenderPrePass* --> RenderMobileBasePass RenderMobileBasePass --> RenderOcclusion* RenderOcclusion* --> RenderDecals* RenderDecals* --> RenderModulatedShadowProjections* RenderModulatedShadowProjections* --> RenderTranslucency* RenderTranslucency* --> PreTonemapMSAA* PreTonemapMSAA* --> [*]

    其中遮擋剔除和GPU廠商相關,比如高通Adreno系列GPU芯片要求在Flush渲染指令和Switch FBO之間:

    Render Opaque -> Render Translucent -> Flush -> Render Queries -> Switch FBO

    那么UE也遵循了Adreno系列芯片的特殊要求,對其的遮擋剔除做了特殊的處理。

    Adreno系列芯片支持TBDR架構的Bin和普通的Direct兩種混合模式的渲染,會在遮擋查詢時自動切換到Direct模式,以降低遮擋查詢的開銷。如果不在Flush渲染指令和Switch FBO之間提交查詢,會卡住整個渲染管線,引發渲染性能下降。

    MSAA由于天然硬件支持且效果和效率達到很好的平衡,是UE在移動端前向渲染的首選抗鋸齒。因此,上述代碼中出現了不少處理MSAA的邏輯,包含顏色和深度紋理及其資源狀態。如果開啟了MSAA,默認情況下是在RHICmdList.EndRenderPass()解析場景顏色(同時將芯片分塊上的數據寫回到系統顯存中),由此獲得抗鋸齒的紋理。移動端的MSAA默認不開啟,但可在以下界面中設置:

    前向渲染支持Gamma空間和HDR(線性空間)兩種顏色空間模式。如果是線性空間,則渲染后期需要色調映射等步驟。默認是HDR,可在項目配置中更改:

    上述代碼的bRequiresMultiPass標明了是否需要專用的渲染Pass繪制半透明物體,決定它的值由以下代碼完成:

    // Engine\Source\Runtime\Renderer\Private\MobileShadingRenderer.cpp
    
    bool FMobileSceneRenderer::RequiresMultiPass(FRHICommandListImmediate& RHICmdList, const FViewInfo& View) const
    {
        // Vulkan uses subpasses
        if (IsVulkanPlatform(ShaderPlatform))
        {
            return false;
        }
    
        // All iOS support frame_buffer_fetch
        if (IsMetalMobilePlatform(ShaderPlatform))
        {
            return false;
        }
    
        if (IsMobileDeferredShadingEnabled(ShaderPlatform))
        {
            // TODO: add GL support
            return true;
        }
        
        // Some Androids support frame_buffer_fetch
        if (IsAndroidOpenGLESPlatform(ShaderPlatform) && (GSupportsShaderFramebufferFetch || GSupportsShaderDepthStencilFetch))
        {
            return false;
        }
            
        // Always render reflection capture in single pass
        if (View.bIsPlanarReflection || View.bIsSceneCapture)
        {
            return false;
        }
    
        // Always render LDR in single pass
        if (!IsMobileHDR())
        {
            return false;
        }
    
        // MSAA depth can't be sampled or resolved, unless we are on PC (no vulkan)
        if (NumMSAASamples > 1 && !IsSimulatedPlatform(ShaderPlatform))
        {
            return false;
        }
    
        return true;
    }
    

    與之類似但意義不同的是bIsMultiViewApplication和bIsMobileMultiViewEnabled標記,標明是否開啟多視圖渲染以及多視圖的個數。只用于VR,由控制臺變量vr.MobileMultiView及圖形API等因素決定。MultiView用于XR,用于優化渲染兩次的情形,它存在Basic和Advanced兩種模式:

    用于優化VR等渲染的MultiView對比圖。上:未采用MultiView模式的渲染,兩個眼睛各自提交繪制指令;中:基礎MultiView模式,復用提交指令,在GPU層復制多一份Command List;下:高級MultiView模式,可以復用DC、Command List、幾何信息。

    bKeepDepthContent標明是否要保留深度內容,決定它的代碼:

    bKeepDepthContent = 
        bRequiresMultiPass || 
        bForceDepthResolve ||
        bRequiresPixelProjectedPlanarRelfectionPass ||
        bSeparateTranslucencyActive ||
        Views[0].bIsReflectionCapture ||
        (bDeferredShading && bPostProcessUsesSceneDepth) ||
        bShouldRenderVelocities ||
        bIsFullPrepassEnabled;
    
    // 帶MSAA的深度從不保留.
    bKeepDepthContent = (NumMSAASamples > 1 ? false : bKeepDepthContent);
    

    上述代碼還揭示了平面反射在移動端的一種特殊渲染方式:Pixel Projected Reflection(PPR)。它的實現原理類似于SSR,但需要的數據更少,只需要場景顏色、深度緩沖和反射區域。它的核心步驟:

    • 計算場景顏色的所有像素在反射平面的鏡像位置。
    • 測試像素的反射是否在反射區域內。
      • 光線投射到鏡像像素位置。
      • 測試交點是否在反射區域內。
    • 如果找到相交點,計算像素在屏幕的鏡像位置。
    • 在交點處寫入鏡像像素的顏色。

    • 如果反射區域內的交點被其它物體遮擋,則剔除此位置的反射。

    PPR效果一覽。

    PPR可以在工程配置中設置:

    12.3.3 RenderDeferred

    UE在4.26在移動端渲染管線增加了延遲渲染分支,并在4.27做了改進和優化。移動端是否開啟延遲著色的特性由以下代碼決定:

    // Engine\Source\Runtime\RenderCore\Private\RenderUtils.cpp
    
    bool IsMobileDeferredShadingEnabled(const FStaticShaderPlatform Platform)
    {
        // 禁用OpenGL的延遲著色.
        if (IsOpenGLPlatform(Platform))
        {
            // needs MRT framebuffer fetch or PLS
            return false;
        }
        
        // 控制臺變量"r.Mobile.ShadingPath"要為1.
        static auto* MobileShadingPathCvar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.Mobile.ShadingPath"));
        return MobileShadingPathCvar->GetValueOnAnyThread() == 1;
    }
    

    簡單地說就是非OpenGL圖形API且控制臺變量r.Mobile.ShadingPath設為1。

    r.Mobile.ShadingPath不可在編輯器動態設置值,只能在項目工程根目錄/Config/DefaultEngine.ini增加以下字段來開啟:

    [/Script/Engine.RendererSettings]

    r.Mobile.ShadingPath=1

    添加以上字段后,重啟UE編輯器,等待shader編譯完成即可預覽移動端延遲著色效果。

    以下是延遲渲染分支FMobileSceneRenderer::RenderDeferred的代碼和解析:

    FRHITexture* FMobileSceneRenderer::RenderDeferred(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> ViewList, const FSortedLightSetSceneInfo& SortedLightSet)
    {
        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
        
        // 準備GBuffer.
        FRHITexture* ColorTargets[4] = {
            SceneContext.GetSceneColorSurface(),
            SceneContext.GetGBufferATexture().GetReference(),
            SceneContext.GetGBufferBTexture().GetReference(),
            SceneContext.GetGBufferCTexture().GetReference()
        };
    
        // RHI是否需要將GBuffer存儲到GPU的系統內存中,并在單獨的渲染通道中進行著色.
        ERenderTargetActions GBufferAction = bRequiresMultiPass ? ERenderTargetActions::Clear_Store : ERenderTargetActions::Clear_DontStore;
        EDepthStencilTargetActions DepthAction = bKeepDepthContent ? EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil : EDepthStencilTargetActions::ClearDepthStencil_DontStoreDepthStencil;
        
        // RT的load/store動作.
        ERenderTargetActions ColorTargetsAction[4] = {ERenderTargetActions::Clear_Store, GBufferAction, GBufferAction, GBufferAction};
        if (bIsFullPrepassEnabled)
        {
            ERenderTargetActions DepthTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetDepthActions(DepthAction)));
            ERenderTargetActions StencilTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetStencilActions(DepthAction)));
            DepthAction = MakeDepthStencilTargetActions(DepthTarget, StencilTarget);
        }
        
        FRHIRenderPassInfo BasePassInfo = FRHIRenderPassInfo();
        int32 ColorTargetIndex = 0;
        for (; ColorTargetIndex < UE_ARRAY_COUNT(ColorTargets); ++ColorTargetIndex)
        {
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].RenderTarget = ColorTargets[ColorTargetIndex];
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].ResolveTarget = nullptr;
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].ArraySlice = -1;
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].MipIndex = 0;
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].Action = ColorTargetsAction[ColorTargetIndex];
        }
        
        if (MobileRequiresSceneDepthAux(ShaderPlatform))
        {
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].RenderTarget = SceneContext.SceneDepthAux->GetRenderTargetItem().ShaderResourceTexture.GetReference();
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].ResolveTarget = nullptr;
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].ArraySlice = -1;
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].MipIndex = 0;
            BasePassInfo.ColorRenderTargets[ColorTargetIndex].Action = GBufferAction;
            ColorTargetIndex++;
        }
    
        BasePassInfo.DepthStencilRenderTarget.DepthStencilTarget = SceneContext.GetSceneDepthSurface();
        BasePassInfo.DepthStencilRenderTarget.ResolveTarget = nullptr;
        BasePassInfo.DepthStencilRenderTarget.Action = DepthAction;
        BasePassInfo.DepthStencilRenderTarget.ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;
            
        BasePassInfo.SubpassHint = ESubpassHint::DeferredShadingSubpass;
        if (!bIsFullPrepassEnabled)
        {
            BasePassInfo.NumOcclusionQueries = ComputeNumOcclusionQueriesToBatch();
            BasePassInfo.bOcclusionQueries = BasePassInfo.NumOcclusionQueries != 0;
        }
        BasePassInfo.ShadingRateTexture = nullptr;
        BasePassInfo.bIsMSAA = false;
        BasePassInfo.MultiViewCount = 0;
    
        RHICmdList.BeginRenderPass(BasePassInfo, TEXT("BasePassRendering"));
        
        if (GIsEditor && !Views[0].bIsSceneCapture)
        {
            DrawClearQuad(RHICmdList, Views[0].BackgroundColor);
        }
    
        // 深度PrePass
        if (!bIsFullPrepassEnabled)
        {
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
            // Depth pre-pass
            RenderPrePass(RHICmdList);
        }
    
        // BasePass: 不透明和鏤空物體.
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Opaque));
        RenderMobileBasePass(RHICmdList, ViewList);
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    
        // 遮擋剔除.
        if (!bIsFullPrepassEnabled)
        {
            // Issue occlusion queries
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
            RenderOcclusion(RHICmdList);
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
    
        // 非多Pass模式
        if (!bRequiresMultiPass)
        {
            // 下個子Pass: SSceneColor + GBuffer寫入, SceneDepth只讀.
            RHICmdList.NextSubpass();
            
            // 渲染貼花.
            if (ViewFamily.EngineShowFlags.Decals)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
                RenderDecals(RHICmdList);
            }
    
            // 下個子Pass: SceneColor寫入, SceneDepth只讀
            RHICmdList.NextSubpass();
            
            // 延遲光照著色.
            MobileDeferredShadingPass(RHICmdList, *Scene, ViewList, SortedLightSet);
            
            // 繪制半透明.
            if (ViewFamily.EngineShowFlags.Translucency)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderTranslucency);
                SCOPE_CYCLE_COUNTER(STAT_TranslucencyDrawTime);
                RenderTranslucency(RHICmdList, ViewList);
                FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
                RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
            }
            
            // 結束渲染Pass.
            RHICmdList.EndRenderPass();
        }
        // 多Pass模式(PC設備模擬的移動端).
        else
        {
            // 結束子pass.
            RHICmdList.NextSubpass();
            RHICmdList.NextSubpass();
            RHICmdList.EndRenderPass();
            
            // SceneColor + GBuffer write, SceneDepth is read only
            {
                for (int32 Index = 0; Index < UE_ARRAY_COUNT(ColorTargets); ++Index)
                {
                    BasePassInfo.ColorRenderTargets[Index].Action = ERenderTargetActions::Load_Store;
                }
                BasePassInfo.DepthStencilRenderTarget.Action = EDepthStencilTargetActions::LoadDepthStencil_StoreDepthStencil;
                BasePassInfo.DepthStencilRenderTarget.ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilRead;
                BasePassInfo.SubpassHint = ESubpassHint::None;
                BasePassInfo.NumOcclusionQueries = 0;
                BasePassInfo.bOcclusionQueries = false;
                
                RHICmdList.BeginRenderPass(BasePassInfo, TEXT("AfterBasePass"));
                
                // 渲染貼花.
                if (ViewFamily.EngineShowFlags.Decals)
                {
                    CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
                    RenderDecals(RHICmdList);
                }
                
                RHICmdList.EndRenderPass();
            }
    
            // SceneColor write, SceneDepth is read only
            {
                FRHIRenderPassInfo ShadingPassInfo(
                    SceneContext.GetSceneColorSurface(),
                    ERenderTargetActions::Load_Store,
                    nullptr,
                    SceneContext.GetSceneDepthSurface(),
                    EDepthStencilTargetActions::LoadDepthStencil_StoreDepthStencil, 
                    nullptr,
                    nullptr,
                    VRSRB_Passthrough,
                    FExclusiveDepthStencil::DepthRead_StencilWrite
                );
                ShadingPassInfo.NumOcclusionQueries = 0;
                ShadingPassInfo.bOcclusionQueries = false;
                
                RHICmdList.BeginRenderPass(ShadingPassInfo, TEXT("MobileShadingPass"));
                
                // 延遲光照著色.
                MobileDeferredShadingPass(RHICmdList, *Scene, ViewList, SortedLightSet);
                
                // 繪制半透明.
                if (ViewFamily.EngineShowFlags.Translucency)
                {
                    CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderTranslucency);
                    SCOPE_CYCLE_COUNTER(STAT_TranslucencyDrawTime);
                    RenderTranslucency(RHICmdList, ViewList);
                    FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
                    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
                }
    
                RHICmdList.EndRenderPass();
            }
        }
    
        return ColorTargets[0];
    }
    

    由上面可知,移動端的延遲渲染管線和PC比較類似,先渲染BasePass,獲得GBuffer幾何信息,再執行光照計算。它們的流程圖如下:

    stateDiagram-v2 [*] --> DrawClearQuad* DrawClearQuad* --> RenderPrePass* RenderPrePass* --> RenderMobileBasePass RenderMobileBasePass --> RenderOcclusion* RenderOcclusion* --> RenderDecals* RenderDecals* --> MobileDeferredShadingPass* MobileDeferredShadingPass* --> RenderTranslucency* RenderTranslucency* --> PreTonemapMSAA* PreTonemapMSAA* --> [*]

    當然,也有和PC不一樣的地方,最明顯的是移動端使用了適配TB(D)R架構的SubPass渲染,使得移動端在渲染PrePass深度、BasePass、光照計算時,讓場景顏色、深度、GBuffer等信息一直在On-Chip的緩沖區中,提升渲染效率,降低設備能耗。

    12.3.3.1 MobileDeferredShadingPass

    延遲渲染光照的過程由MobileDeferredShadingPass擔當:

    void MobileDeferredShadingPass(
        FRHICommandListImmediate& RHICmdList, 
        const FScene& Scene, 
        const TArrayView<const FViewInfo*> PassViews, 
        const FSortedLightSetSceneInfo &SortedLightSet)
    {
        SCOPED_DRAW_EVENT(RHICmdList, MobileDeferredShading);
    
        const FViewInfo& View0 = *PassViews[0];
    
        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
        // 創建Uniform Buffer.
        FUniformBufferRHIRef PassUniformBuffer = CreateMobileSceneTextureUniformBuffer(RHICmdList);
        FUniformBufferStaticBindings GlobalUniformBuffers(PassUniformBuffer);
        SCOPED_UNIFORM_BUFFER_GLOBAL_BINDINGS(RHICmdList, GlobalUniformBuffers);
        // 設置視口.
        RHICmdList.SetViewport(View0.ViewRect.Min.X, View0.ViewRect.Min.Y, 0.0f, View0.ViewRect.Max.X, View0.ViewRect.Max.Y, 1.0f);
    
        // 光照的默認材質.
        FCachedLightMaterial DefaultMaterial;
        DefaultMaterial.MaterialProxy = UMaterial::GetDefaultMaterial(MD_LightFunction)->GetRenderProxy();
        DefaultMaterial.Material = DefaultMaterial.MaterialProxy->GetMaterialNoFallback(ERHIFeatureLevel::ES3_1);
        check(DefaultMaterial.Material);
    
        // 繪制平行光.
        RenderDirectLight(RHICmdList, Scene, View0, DefaultMaterial);
    
        if (GMobileUseClusteredDeferredShading == 0)
        {
            // 渲染非分簇的簡單光源.
            RenderSimpleLights(RHICmdList, Scene, PassViews, SortedLightSet, DefaultMaterial);
        }
    
        // 渲染非分簇的局部光源.
        int32 NumLights = SortedLightSet.SortedLights.Num();
        int32 StandardDeferredStart = SortedLightSet.SimpleLightsEnd;
        if (GMobileUseClusteredDeferredShading != 0)
        {
            StandardDeferredStart = SortedLightSet.ClusteredSupportedEnd;
        }
    
        // 渲染局部光源.
        for (int32 LightIdx = StandardDeferredStart; LightIdx < NumLights; ++LightIdx)
        {
            const FSortedLightSceneInfo& SortedLight = SortedLightSet.SortedLights[LightIdx];
            const FLightSceneInfo& LightSceneInfo = *SortedLight.LightSceneInfo;
            RenderLocalLight(RHICmdList, Scene, View0, LightSceneInfo, DefaultMaterial);
        }
    }
    

    下面繼續分析渲染不同類型光源的接口:

    // Engine\Source\Runtime\Renderer\Private\MobileDeferredShadingPass.cpp
    
    // 渲染平行光
    static void RenderDirectLight(FRHICommandListImmediate& RHICmdList, const FScene& Scene, const FViewInfo& View, const FCachedLightMaterial& DefaultLightMaterial)
    {
        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
    
        // 查找第一個平行光.
        FLightSceneInfo* DirectionalLight = nullptr;
        for (int32 ChannelIdx = 0; ChannelIdx < UE_ARRAY_COUNT(Scene.MobileDirectionalLights) && !DirectionalLight; ChannelIdx++)
        {
            DirectionalLight = Scene.MobileDirectionalLights[ChannelIdx];
        }
    
        // 渲染狀態.
        FGraphicsPipelineStateInitializer GraphicsPSOInit;
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
        // 增加自發光到SceneColor.
        GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGB, BO_Add, BF_One, BF_One>::GetRHI();
        GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
        // 只繪制默認光照模型(MSM_DefaultLit)的像素.
        uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
        GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<
                                            false, CF_Always,
                                            true, CF_Equal, SO_Keep, SO_Keep, SO_Keep,        
                                            false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
                                            GET_STENCIL_MOBILE_SM_MASK(0x7), 0x00>::GetRHI(); // 4 bits for shading models
        
        // 處理VS.
        TShaderMapRef<FPostProcessVS> VertexShader(View.ShaderMap);
        
        const FMaterialRenderProxy* LightFunctionMaterialProxy = nullptr;
        if (View.Family->EngineShowFlags.LightFunctions && DirectionalLight)
        {
            LightFunctionMaterialProxy = DirectionalLight->Proxy->GetLightFunctionMaterial();
        }
        FMobileDirectLightFunctionPS::FPermutationDomain PermutationVector = FMobileDirectLightFunctionPS::BuildPermutationVector(View, DirectionalLight != nullptr);
        FCachedLightMaterial LightMaterial;
        TShaderRef<FMobileDirectLightFunctionPS> PixelShader;
        GetLightMaterial(DefaultLightMaterial, LightFunctionMaterialProxy, PermutationVector.ToDimensionValueId(), LightMaterial, PixelShader);
        
        GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
        GraphicsPSOInit.PrimitiveType = PT_TriangleList;
        SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
    
        // 處理PS.
        FMobileDirectLightFunctionPS::FParameters PassParameters;
        PassParameters.Forward = View.ForwardLightingResources->ForwardLightDataUniformBuffer;
        PassParameters.MobileDirectionalLight = Scene.UniformBuffers.MobileDirectionalLightUniformBuffers[1];
        PassParameters.ReflectionCaptureData = Scene.UniformBuffers.ReflectionCaptureUniformBuffer;
        FReflectionUniformParameters ReflectionUniformParameters;
        SetupReflectionUniformParameters(View, ReflectionUniformParameters);
        PassParameters.ReflectionsParameters = CreateUniformBufferImmediate(ReflectionUniformParameters, UniformBuffer_SingleDraw);
        PassParameters.LightFunctionParameters = FVector4(1.0f, 1.0f, 0.0f, 0.0f);
        if (DirectionalLight)
        {
            const bool bUseMovableLight = DirectionalLight && !DirectionalLight->Proxy->HasStaticShadowing();
            PassParameters.LightFunctionParameters2 = FVector(DirectionalLight->Proxy->GetLightFunctionFadeDistance(), DirectionalLight->Proxy->GetLightFunctionDisabledBrightness(), bUseMovableLight ? 1.0f : 0.0f);
            const FVector Scale = DirectionalLight->Proxy->GetLightFunctionScale();
            // Switch x and z so that z of the user specified scale affects the distance along the light direction
            const FVector InverseScale = FVector(1.f / Scale.Z, 1.f / Scale.Y, 1.f / Scale.X);
            PassParameters.WorldToLight = DirectionalLight->Proxy->GetWorldToLight() * FScaleMatrix(FVector(InverseScale));
        }
        FMobileDirectLightFunctionPS::SetParameters(RHICmdList, PixelShader, View, LightMaterial.MaterialProxy, *LightMaterial.Material, PassParameters);
        
        RHICmdList.SetStencilRef(StencilRef);
                
        const FIntPoint TargetSize = SceneContext.GetBufferSizeXY();
        
        // 用全屏幕的矩形繪制.
        DrawRectangle(
            RHICmdList, 
            0, 0, 
            View.ViewRect.Width(), View.ViewRect.Height(), 
            View.ViewRect.Min.X, View.ViewRect.Min.Y, 
            View.ViewRect.Width(), View.ViewRect.Height(),
            FIntPoint(View.ViewRect.Width(), View.ViewRect.Height()), 
            TargetSize, 
            VertexShader);
    }
    
    // 渲染非分簇模式的簡單光源.
    static void RenderSimpleLights(
        FRHICommandListImmediate& RHICmdList, 
        const FScene& Scene, 
        const TArrayView<const FViewInfo*> PassViews, 
        const FSortedLightSetSceneInfo &SortedLightSet, 
        const FCachedLightMaterial& DefaultMaterial)
    {
        const FSimpleLightArray& SimpleLights = SortedLightSet.SimpleLights;
        const int32 NumViews = PassViews.Num();
        const FViewInfo& View0 = *PassViews[0];
    
        // 處理VS.
        TShaderMapRef<TDeferredLightVS<true>> VertexShader(View0.ShaderMap);
        TShaderRef<FMobileRadialLightFunctionPS> PixelShaders[2];
        {
            const FMaterialShaderMap* MaterialShaderMap = DefaultMaterial.Material->GetRenderingThreadShaderMap();
            FMobileRadialLightFunctionPS::FPermutationDomain PermutationVector;
            PermutationVector.Set<FMobileRadialLightFunctionPS::FSpotLightDim>(false);
            PermutationVector.Set<FMobileRadialLightFunctionPS::FIESProfileDim>(false);
            PermutationVector.Set<FMobileRadialLightFunctionPS::FInverseSquaredDim>(false);
            PixelShaders[0] = MaterialShaderMap->GetShader<FMobileRadialLightFunctionPS>(PermutationVector);
            PermutationVector.Set<FMobileRadialLightFunctionPS::FInverseSquaredDim>(true);
            PixelShaders[1] = MaterialShaderMap->GetShader<FMobileRadialLightFunctionPS>(PermutationVector);
        }
    
        // 設置PSO.
        FGraphicsPipelineStateInitializer GraphicsPSOLight[2];
        {
            SetupSimpleLightPSO(RHICmdList, View0, VertexShader, PixelShaders[0], GraphicsPSOLight[0]);
            SetupSimpleLightPSO(RHICmdList, View0, VertexShader, PixelShaders[1], GraphicsPSOLight[1]);
        }
        
        // 設置模板緩沖.
        FGraphicsPipelineStateInitializer GraphicsPSOLightMask;
        {
            RHICmdList.ApplyCachedRenderTargets(GraphicsPSOLightMask);
            GraphicsPSOLightMask.PrimitiveType = PT_TriangleList;
            GraphicsPSOLightMask.BlendState = TStaticBlendStateWriteMask<CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE>::GetRHI();
            GraphicsPSOLightMask.RasterizerState = View0.bReverseCulling ? TStaticRasterizerState<FM_Solid, CM_CCW>::GetRHI() : TStaticRasterizerState<FM_Solid, CM_CW>::GetRHI();
            // set stencil to 1 where depth test fails
            GraphicsPSOLightMask.DepthStencilState = TStaticDepthStencilState<
                false, CF_DepthNearOrEqual,
                true, CF_Always, SO_Keep, SO_Replace, SO_Keep,        
                false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
                0x00, STENCIL_SANDBOX_MASK>::GetRHI();
            GraphicsPSOLightMask.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
            GraphicsPSOLightMask.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
            GraphicsPSOLightMask.BoundShaderState.PixelShaderRHI = nullptr;
        }
        
        // 遍歷所有簡單光源列表, 執行著色計算.
        for (int32 LightIndex = 0; LightIndex < SimpleLights.InstanceData.Num(); LightIndex++)
        {
            const FSimpleLightEntry& SimpleLight = SimpleLights.InstanceData[LightIndex];
            for (int32 ViewIndex = 0; ViewIndex < NumViews; ViewIndex++)
            {
                const FViewInfo& View = *PassViews[ViewIndex];
                const FSimpleLightPerViewEntry& SimpleLightPerViewData = SimpleLights.GetViewDependentData(LightIndex, ViewIndex, NumViews);
                const FSphere LightBounds(SimpleLightPerViewData.Position, SimpleLight.Radius);
                
                if (NumViews > 1)
                {
                    // set viewports only we we have more than one 
                    // otherwise it is set at the start of the pass
                    RHICmdList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0.0f, View.ViewRect.Max.X, View.ViewRect.Max.Y, 1.0f);
                }
    
                // 渲染光源遮罩.
                SetGraphicsPipelineState(RHICmdList, GraphicsPSOLightMask);
                VertexShader->SetSimpleLightParameters(RHICmdList, View, LightBounds);
                RHICmdList.SetStencilRef(1);
                StencilingGeometry::DrawSphere(RHICmdList);
                            
                // 渲染光源.
                FMobileRadialLightFunctionPS::FParameters PassParameters;
                FDeferredLightUniformStruct DeferredLightUniformsValue;
                SetupSimpleDeferredLightParameters(SimpleLight, SimpleLightPerViewData, DeferredLightUniformsValue);
                PassParameters.DeferredLightUniforms = TUniformBufferRef<FDeferredLightUniformStruct>::CreateUniformBufferImmediate(DeferredLightUniformsValue, EUniformBufferUsage::UniformBuffer_SingleFrame);
                PassParameters.IESTexture = GWhiteTexture->TextureRHI;
                PassParameters.IESTextureSampler = GWhiteTexture->SamplerStateRHI;
                if (SimpleLight.Exponent == 0)
                {
                    SetGraphicsPipelineState(RHICmdList, GraphicsPSOLight[1]);
                    FMobileRadialLightFunctionPS::SetParameters(RHICmdList, PixelShaders[1], View, DefaultMaterial.MaterialProxy, *DefaultMaterial.Material, PassParameters);
                }
                else
                {
                    SetGraphicsPipelineState(RHICmdList, GraphicsPSOLight[0]);
                    FMobileRadialLightFunctionPS::SetParameters(RHICmdList, PixelShaders[0], View, DefaultMaterial.MaterialProxy, *DefaultMaterial.Material, PassParameters);
                }
                VertexShader->SetSimpleLightParameters(RHICmdList, View, LightBounds);
                
                // 只繪制默認光照模型(MSM_DefaultLit)的像素.
                uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
                RHICmdList.SetStencilRef(StencilRef);
    
                // 用球體渲染光源(點光源和聚光燈), 以快速剔除光源影響之外的像素.
                StencilingGeometry::DrawSphere(RHICmdList);
            }
        }
    }
    
    // 渲染局部光源.
    static void RenderLocalLight(
        FRHICommandListImmediate& RHICmdList, 
        const FScene& Scene, 
        const FViewInfo& View, 
        const FLightSceneInfo& LightSceneInfo, 
        const FCachedLightMaterial& DefaultLightMaterial)
    {
        if (!LightSceneInfo.ShouldRenderLight(View))
        {
            return;
        }
    
        // 忽略非局部光源(光源和聚光燈之外的光源).
        const uint8 LightType = LightSceneInfo.Proxy->GetLightType();
        const bool bIsSpotLight = LightType == LightType_Spot;
        const bool bIsPointLight = LightType == LightType_Point;
        if (!bIsSpotLight && !bIsPointLight)
        {
            return;
        }
        
        // 繪制光源模板.
        if (GMobileUseLightStencilCulling != 0)
        {
            RenderLocalLight_StencilMask(RHICmdList, Scene, View, LightSceneInfo);
        }
    
        // 處理IES光照.
        bool bUseIESTexture = false;
        FTexture* IESTextureResource = GWhiteTexture;
        if (View.Family->EngineShowFlags.TexturedLightProfiles && LightSceneInfo.Proxy->GetIESTextureResource())
        {
            IESTextureResource = LightSceneInfo.Proxy->GetIESTextureResource();
            bUseIESTexture = true;
        }
            
        FGraphicsPipelineStateInitializer GraphicsPSOInit;
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
        GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_One, BO_Add, BF_One, BF_One>::GetRHI();
        GraphicsPSOInit.PrimitiveType = PT_TriangleList;
        const FSphere LightBounds = LightSceneInfo.Proxy->GetBoundingSphere();
        
        // 設置光源光柵化和深度狀態.
        if (GMobileUseLightStencilCulling != 0)
        {
            SetLocalLightRasterizerAndDepthState_StencilMask(GraphicsPSOInit, View);
        }
        else
        {
            SetLocalLightRasterizerAndDepthState(GraphicsPSOInit, View, LightBounds);
        }
    
        // 設置VS
        TShaderMapRef<TDeferredLightVS<true>> VertexShader(View.ShaderMap);
            
        const FMaterialRenderProxy* LightFunctionMaterialProxy = nullptr;
        if (View.Family->EngineShowFlags.LightFunctions)
        {
            LightFunctionMaterialProxy = LightSceneInfo.Proxy->GetLightFunctionMaterial();
        }
        FMobileRadialLightFunctionPS::FPermutationDomain PermutationVector;
        PermutationVector.Set<FMobileRadialLightFunctionPS::FSpotLightDim>(bIsSpotLight);
        PermutationVector.Set<FMobileRadialLightFunctionPS::FInverseSquaredDim>(LightSceneInfo.Proxy->IsInverseSquared());
        PermutationVector.Set<FMobileRadialLightFunctionPS::FIESProfileDim>(bUseIESTexture);
        FCachedLightMaterial LightMaterial;
        TShaderRef<FMobileRadialLightFunctionPS> PixelShader;
        GetLightMaterial(DefaultLightMaterial, LightFunctionMaterialProxy, PermutationVector.ToDimensionValueId(), LightMaterial, PixelShader);
                
        GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
        SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
    
        VertexShader->SetParameters(RHICmdList, View, &LightSceneInfo);
    
        // 設置PS.
        FMobileRadialLightFunctionPS::FParameters PassParameters;
        PassParameters.DeferredLightUniforms = TUniformBufferRef<FDeferredLightUniformStruct>::CreateUniformBufferImmediate(GetDeferredLightParameters(View, LightSceneInfo), EUniformBufferUsage::UniformBuffer_SingleFrame);
        PassParameters.IESTexture = IESTextureResource->TextureRHI;
        PassParameters.IESTextureSampler = IESTextureResource->SamplerStateRHI;
        const float TanOuterAngle = bIsSpotLight ? FMath::Tan(LightSceneInfo.Proxy->GetOuterConeAngle()) : 1.0f;
        PassParameters.LightFunctionParameters = FVector4(TanOuterAngle, 1.0f /*ShadowFadeFraction*/, bIsSpotLight ? 1.0f : 0.0f, bIsPointLight ? 1.0f : 0.0f);
        PassParameters.LightFunctionParameters2 = FVector(LightSceneInfo.Proxy->GetLightFunctionFadeDistance(), LightSceneInfo.Proxy->GetLightFunctionDisabledBrightness(),    0.0f);
        const FVector Scale = LightSceneInfo.Proxy->GetLightFunctionScale();
        // Switch x and z so that z of the user specified scale affects the distance along the light direction
        const FVector InverseScale = FVector(1.f / Scale.Z, 1.f / Scale.Y, 1.f / Scale.X);
        PassParameters.WorldToLight = LightSceneInfo.Proxy->GetWorldToLight() * FScaleMatrix(FVector(InverseScale));
        FMobileRadialLightFunctionPS::SetParameters(RHICmdList, PixelShader, View, LightMaterial.MaterialProxy, *LightMaterial.Material, PassParameters);
    
        // 只繪制默認光照模型(MSM_DefaultLit)的像素.
        uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
        RHICmdList.SetStencilRef(StencilRef);
    
        // 點光源用球體繪制.
        if (LightType == LightType_Point)
        {
            StencilingGeometry::DrawSphere(RHICmdList);
        }
        // 聚光燈用錐體繪制.
        else // LightType_Spot
        {
            StencilingGeometry::DrawCone(RHICmdList);
        }
    }
    

    繪制光源時,按光源類型劃分為三個步驟:平行光、非分簇簡單光源、局部光源(點光源和聚光燈)。需要注意的是,移動端只支持默認光照模型(MSM_DefaultLit)的計算,其它高級光照模型(頭發、次表面散射、清漆、眼睛、布料等)暫不支持。

    繪制平行光時,最多只能繪制1個,采用的是全屏幕矩形繪制,支持若干級CSM陰影。

    繪制非分簇簡單光源時,無論是點光源還是聚光燈,都采用球體繪制,不支持陰影。

    繪制局部光源時,會復雜許多,先繪制局部光源模板緩沖,再設置光柵化和深度狀態,最后才繪制光源。其中點光源采用球體繪制,不支持陰影;聚光燈采用錐體繪制,可以支持陰影,默認情況下,聚光燈不支持動態光影計算,需要在工程配置中開啟:

    此外,是否開啟模板剔除光源不相交的像素由GMobileUseLightStencilCulling決定,而GMobileUseLightStencilCulling又由r.Mobile.UseLightStencilCulling決定,默認為1(即開啟狀態)。渲染光源的模板緩沖代碼如下:

    static void RenderLocalLight_StencilMask(FRHICommandListImmediate& RHICmdList, const FScene& Scene, const FViewInfo& View, const FLightSceneInfo& LightSceneInfo)
    {
        const uint8 LightType = LightSceneInfo.Proxy->GetLightType();
    
        FGraphicsPipelineStateInitializer GraphicsPSOInit;
        // 應用緩存好的RT(顏色/深度等).
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
        GraphicsPSOInit.PrimitiveType = PT_TriangleList;
        // 禁用所有RT的寫操作.
        GraphicsPSOInit.BlendState = TStaticBlendStateWriteMask<CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE>::GetRHI();
        GraphicsPSOInit.RasterizerState = View.bReverseCulling ? TStaticRasterizerState<FM_Solid, CM_CCW>::GetRHI() : TStaticRasterizerState<FM_Solid, CM_CW>::GetRHI();
        // 如果深度測試失敗, 則寫入模板緩沖值為1.
        GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<
            false, CF_DepthNearOrEqual,
            true, CF_Always, SO_Keep, SO_Replace, SO_Keep,        
            false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
            0x00, 
            // 注意只寫入Pass專用的沙盒(SANBOX)位, 即模板緩沖的索引為0的位.
            STENCIL_SANDBOX_MASK>::GetRHI();
        
        // 繪制光源模板的VS是TDeferredLightVS.
        TShaderMapRef<TDeferredLightVS<true> > VertexShader(View.ShaderMap);
        GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
        GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
        // PS為空.
        GraphicsPSOInit.BoundShaderState.PixelShaderRHI = nullptr;
    
        SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
        VertexShader->SetParameters(RHICmdList, View, &LightSceneInfo);
        // 模板值為1.
        RHICmdList.SetStencilRef(1);
    
        // 根據不同光源用不同形狀繪制.
        if (LightType == LightType_Point)
        {
            StencilingGeometry::DrawSphere(RHICmdList);
        }
        else // LightType_Spot
        {
            StencilingGeometry::DrawCone(RHICmdList);
        }
    }
    

    每個局部光源首先繪制光源范圍內的Mask,再計算通過了Stencil測試(Early-Z)的像素的光照。具體的剖析過程以下圖的聚光燈為例:

    上:場景中一盞等待渲染的聚光燈;中:利用模板Pass繪制出的模板Mask(白色區域),標記了屏幕空間中和聚光燈形狀重疊且深度更近的像素 ;下:對有效像素進行光照計算后的效果。

    對有效像素進行光照計算時,使用的DepthStencil狀態如下:

    翻譯成文字就是,執行光照的像素必須在光源形狀體之內,光源形狀之外的像素會被剔除。模板Pass標記的是比光源形狀深度更近的像素(光源形狀體之外的像素),光源繪制Pass通過模板測試剔除模板Pass標記的像素,然后再通過深度測試找出在光源形狀體內的像素,從而提升光照計算效率。

    移動端的這種光源模板裁剪(Light Stencil Culling)技術和Siggraph2020的Unity演講Deferred Shading in Unity URP提及的基于模板的光照計算相似(思想一致,但做法可能不完全一樣)。該論文還提出了更加契合光源形狀的幾何體模擬:

    以及對比了各種光源計算方法在PC和移動端的性能,下面是Mali GPU的對比圖:

    Mali Gpu在使用不同光照渲染技術的性能對比,可見在移動端,基于模板裁剪的光照算法要優于常規和分塊算法。

    值得一提的是,光源模板裁剪技術結合GPU的Early-Z技術,將極大提升光照渲染性能。而當前主流的移動端GPU都支持Early-Z技術,也為光源模板裁剪的應用奠定了基礎。

    UE目前實現的光源裁剪算法興許還有改進的空間,比如背向光源的像素(下圖紅框所示)其實也是可以不計算的。(但如何快速有效地找到背向光源的像素又是一個問題)

    12.3.3.2 MobileBasePassShader

    本節主要闡述移動端BasePass涉及的shader,包括VS和PS。先看VS:

    // Engine\Shaders\Private\MobileBasePassVertexShader.usf
    
    (......)
    
    struct FMobileShadingBasePassVSToPS
    {
        FVertexFactoryInterpolantsVSToPS FactoryInterpolants;
        FMobileBasePassInterpolantsVSToPS BasePassInterpolants;
        float4 Position : SV_POSITION;
    };
    
    #define FMobileShadingBasePassVSOutput FMobileShadingBasePassVSToPS
    #define VertexFactoryGetInterpolants VertexFactoryGetInterpolantsVSToPS
    
    // VS主入口.
    void Main(
        FVertexFactoryInput Input
        , out FMobileShadingBasePassVSOutput Output
    #if INSTANCED_STEREO
        , uint InstanceId : SV_InstanceID
        , out uint LayerIndex : SV_RenderTargetArrayIndex
    #elif MOBILE_MULTI_VIEW
        , in uint ViewId : SV_ViewID
    #endif
        )
    {
    // 立體視圖模式.
    #if INSTANCED_STEREO
        const uint EyeIndex = GetEyeIndex(InstanceId);
        ResolvedView = ResolveView(EyeIndex);
        LayerIndex = EyeIndex;
        Output.BasePassInterpolants.MultiViewId = float(EyeIndex);
    // 多視圖模式.
    #elif MOBILE_MULTI_VIEW
        #if COMPILER_GLSL_ES3_1
            const int MultiViewId = int(ViewId);
            ResolvedView = ResolveView(uint(MultiViewId));
            Output.BasePassInterpolants.MultiViewId = float(MultiViewId);
        #else
            ResolvedView = ResolveView(ViewId);
            Output.BasePassInterpolants.MultiViewId = float(ViewId);
        #endif
    #else
        ResolvedView = ResolveView();
    #endif
    
        // 初始化打包的插值數據.
    #if PACK_INTERPOLANTS
        float4 PackedInterps[NUM_VF_PACKED_INTERPOLANTS];
        UNROLL 
        for(int i = 0; i < NUM_VF_PACKED_INTERPOLANTS; ++i)
        {
            PackedInterps[i] = 0;
        }
    #endif
    
        // 處理頂點工廠數據.
        FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
        float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
        float4 WorldPosition = WorldPositionExcludingWPO;
    
        // 獲取材質的頂點數據, 處理坐標等.
        half3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);    
        FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal);
    
        half3 WorldPositionOffset = GetMaterialWorldPositionOffset(VertexParameters);
        
        WorldPosition.xyz += WorldPositionOffset;
    
        float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition);
        Output.Position = mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip);
        Output.BasePassInterpolants.PixelPosition = WorldPosition;
    
    #if USE_WORLD_POSITION_EXCLUDING_SHADER_OFFSETS
        Output.BasePassInterpolants.PixelPositionExcludingWPO = WorldPositionExcludingWPO.xyz;
    #endif
    
        // 裁剪面.
    #if USE_PS_CLIP_PLANE
        Output.BasePassInterpolants.OutClipDistance = dot(ResolvedView.GlobalClippingPlane, float4(WorldPosition.xyz - ResolvedView.PreViewTranslation.xyz, 1));
    #endif
    
        // 頂點霧.
    #if USE_VERTEX_FOG
        float4 VertexFog = CalculateHeightFog(WorldPosition.xyz - ResolvedView.TranslatedWorldCameraOrigin);
    
        #if PROJECT_SUPPORT_SKY_ATMOSPHERE && MATERIAL_IS_SKY==0 // Do not apply aerial perpsective on sky materials
            if (ResolvedView.SkyAtmosphereApplyCameraAerialPerspectiveVolume > 0.0f)
            {
                const float OneOverPreExposure = USE_PREEXPOSURE ? ResolvedView.OneOverPreExposure : 1.0f;
                // Sample the aerial perspective (AP). It is also blended under the VertexFog parameter.
                VertexFog = GetAerialPerspectiveLuminanceTransmittanceWithFogOver(
                    ResolvedView.RealTimeReflectionCapture, ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeSizeAndInvSize,
                    Output.Position, WorldPosition.xyz*CM_TO_SKY_UNIT, ResolvedView.TranslatedWorldCameraOrigin*CM_TO_SKY_UNIT,
                    View.CameraAerialPerspectiveVolume, View.CameraAerialPerspectiveVolumeSampler,
                    ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthResolutionInv,
                    ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthResolution,
                    ResolvedView.SkyAtmosphereAerialPerspectiveStartDepthKm,
                    ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthSliceLengthKm,
                    ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthSliceLengthKmInv,
                    OneOverPreExposure, VertexFog);
            }
        #endif
    
        #if PACK_INTERPOLANTS
            PackedInterps[0] = VertexFog;
        #else
            Output.BasePassInterpolants.VertexFog = VertexFog;
        #endif // PACK_INTERPOLANTS
    #endif // USE_VERTEX_FOG
    
        (......)
    
        // 獲取待插值的數據.
        Output.FactoryInterpolants = VertexFactoryGetInterpolants(Input, VFIntermediates, VertexParameters);
    
        Output.BasePassInterpolants.PixelPosition.w = Output.Position.w;
    
        // 打包插值數據.
    #if PACK_INTERPOLANTS
        VertexFactoryPackInterpolants(Output.FactoryInterpolants, PackedInterps);
    #endif // PACK_INTERPOLANTS
    
    #if !OUTPUT_MOBILE_HDR && COMPILER_GLSL_ES3_1
        Output.Position.y *= -1;
    #endif
    }
    

    以上可知,視圖實例會根據立體繪制、多視圖和普通模式不同而不同處理。支持頂點霧,但默認是關閉的,需要在工程配置內開啟。

    存在打包插值模式,為了壓縮VS到PS之間的插值消耗和帶寬。是否開啟由宏PACK_INTERPOLANTS決定,它的定義如下:

    // Engine\Shaders\Private\MobileBasePassCommon.ush
    
    #define PACK_INTERPOLANTS (USE_VERTEX_FOG && NUM_VF_PACKED_INTERPOLANTS > 0 && (ES3_1_PROFILE))
    

    也就是說,只有開啟頂點霧、存在頂點工廠打包插值數據且是OpenGLES3.1著色平臺才開啟打包插值的特性。相比PC端的BasePass的VS,移動端的做了大量的簡化,可以簡單地認為只是PC端的一個很小的子集。下面繼續分析PS:

    // Engine\Shaders\Private\MobileBasePassVertexShader.usf
    
    #include "Common.ush"
    
    // 各類宏定義.
    #define MobileSceneTextures MobileBasePass.SceneTextures
    #define EyeAdaptationStruct MobileBasePass
    
    (......)
    
    // 最接近被渲染對象的場景的預歸一化捕獲(完全粗糙材質不支持)
    #if !FULLY_ROUGH
        #if HQ_REFLECTIONS
        #define MAX_HQ_REFLECTIONS 3
        TextureCube ReflectionCubemap0;
        SamplerState ReflectionCubemapSampler0;
        TextureCube ReflectionCubemap1;
        SamplerState ReflectionCubemapSampler1;
        TextureCube ReflectionCubemap2;
        SamplerState ReflectionCubemapSampler2;
        // x,y,z - inverted average brightness for 0, 1, 2; w - sky cube texture max mips.
        float4 ReflectionAverageBrigtness;
        float4 ReflectanceMaxValueRGBM;
        float4 ReflectionPositionsAndRadii[MAX_HQ_REFLECTIONS];
            #if ALLOW_CUBE_REFLECTIONS
            float4x4 CaptureBoxTransformArray[MAX_HQ_REFLECTIONS];
            float4 CaptureBoxScalesArray[MAX_HQ_REFLECTIONS];
            #endif
        #endif
    #endif
    
    // 反射球/IBL等接口.
    half4 GetPlanarReflection(float3 WorldPosition, half3 WorldNormal, half Roughness);
    half MobileComputeMixingWeight(half IndirectIrradiance, half AverageBrightness, half Roughness);
    half3 GetLookupVectorForBoxCaptureMobile(half3 ReflectionVector, ...);
    half3 GetLookupVectorForSphereCaptureMobile(half3 ReflectionVector, ...);
    void GatherSpecularIBL(FMaterialPixelParameters MaterialParameters, ...);
    void BlendReflectionCaptures(FMaterialPixelParameters MaterialParameters, ...)
    half3 GetImageBasedReflectionLighting(FMaterialPixelParameters MaterialParameters, ...);
    
    // 其它接口.
    half3 FrameBufferBlendOp(half4 Source);
    bool UseCSM();
    void ApplyPixelDepthOffsetForMobileBasePass(inout FMaterialPixelParameters MaterialParameters, FPixelMaterialInputs PixelMaterialInputs, out float OutDepth);
    
    // 累積動態點光源.
    #if MAX_DYNAMIC_POINT_LIGHTS > 0
    void AccumulateLightingOfDynamicPointLight(
        FMaterialPixelParameters MaterialParameters, 
        FMobileShadingModelContext ShadingModelContext,
        FGBufferData GBuffer,
        float4 LightPositionAndInvRadius, 
        float4 LightColorAndFalloffExponent, 
        float4 SpotLightDirectionAndSpecularScale, 
        float4 SpotLightAnglesAndSoftTransitionScaleAndLightShadowType, 
        #if SUPPORT_SPOTLIGHTS_SHADOW
        FPCFSamplerSettings Settings,
        float4 SpotLightShadowSharpenAndShadowFadeFraction,
        float4 SpotLightShadowmapMinMax,
        float4x4 SpotLightShadowWorldToShadowMatrix,
        #endif
        inout half3 Color)
    {
        uint LightShadowType = SpotLightAnglesAndSoftTransitionScaleAndLightShadowType.w;
        float FadedShadow = 1.0f;
    
        // 計算聚光燈陰影.
    #if SUPPORT_SPOTLIGHTS_SHADOW
        if ((LightShadowType & LightShadowType_Shadow) == LightShadowType_Shadow)
        {
    
            float4 HomogeneousShadowPosition = mul(float4(MaterialParameters.AbsoluteWorldPosition, 1), SpotLightShadowWorldToShadowMatrix);
            float2 ShadowUVs = HomogeneousShadowPosition.xy / HomogeneousShadowPosition.w;
            if (all(ShadowUVs >= SpotLightShadowmapMinMax.xy && ShadowUVs <= SpotLightShadowmapMinMax.zw))
            {
                // Clamp pixel depth in light space for shadowing opaque, because areas of the shadow depth buffer that weren't rendered to will have been cleared to 1
                // We want to force the shadow comparison to result in 'unshadowed' in that case, regardless of whether the pixel being shaded is in front or behind that plane
                float LightSpacePixelDepthForOpaque = min(HomogeneousShadowPosition.z, 0.99999f);
                Settings.SceneDepth = LightSpacePixelDepthForOpaque;
                Settings.TransitionScale = SpotLightAnglesAndSoftTransitionScaleAndLightShadowType.z;
    
                half Shadow = MobileShadowPCF(ShadowUVs, Settings);
    
                Shadow = saturate((Shadow - 0.5) * SpotLightShadowSharpenAndShadowFadeFraction.x + 0.5);
    
                FadedShadow = lerp(1.0f, Square(Shadow), SpotLightShadowSharpenAndShadowFadeFraction.y);
            }
        }
    #endif
    
        // 計算光照.
        if ((LightShadowType & ValidLightType) != 0)
        {
            float3 ToLight = LightPositionAndInvRadius.xyz - MaterialParameters.AbsoluteWorldPosition;
            float DistanceSqr = dot(ToLight, ToLight);
            float3 L = ToLight * rsqrt(DistanceSqr);
            half3 PointH = normalize(MaterialParameters.CameraVector + L);
    
            half PointNoL = max(0, dot(MaterialParameters.WorldNormal, L));
            half PointNoH = max(0, dot(MaterialParameters.WorldNormal, PointH));
    
            // 計算光源的衰減.
            float Attenuation;
            if (LightColorAndFalloffExponent.w == 0)
            {
                // Sphere falloff (technically just 1/d2 but this avoids inf)
                Attenuation = 1 / (DistanceSqr + 1);
    
                float LightRadiusMask = Square(saturate(1 - Square(DistanceSqr * (LightPositionAndInvRadius.w * LightPositionAndInvRadius.w))));
                Attenuation *= LightRadiusMask;
            }
            else
            {
                Attenuation = RadialAttenuation(ToLight * LightPositionAndInvRadius.w, LightColorAndFalloffExponent.w);
            }
    
    #if PROJECT_MOBILE_ENABLE_MOVABLE_SPOTLIGHTS
            if ((LightShadowType & LightShadowType_SpotLight) == LightShadowType_SpotLight)
            {
                Attenuation *= SpotAttenuation(L, -SpotLightDirectionAndSpecularScale.xyz, SpotLightAnglesAndSoftTransitionScaleAndLightShadowType.xy) * FadedShadow;
            }
    #endif
    
            // 累加光照結果.
    #if !FULLY_ROUGH
            FMobileDirectLighting Lighting = MobileIntegrateBxDF(ShadingModelContext, GBuffer, PointNoL, MaterialParameters.CameraVector, PointH, PointNoH);
            Color += min(65000.0, (Attenuation) * LightColorAndFalloffExponent.rgb * (1.0 / PI) * (Lighting.Diffuse + Lighting.Specular * SpotLightDirectionAndSpecularScale.w));
    #else
            Color += (Attenuation * PointNoL) * LightColorAndFalloffExponent.rgb * (1.0 / PI) * ShadingModelContext.DiffuseColor;
    #endif
        }
    }
    #endif
    
    (......)
    
    // 計算非直接光照.
    half ComputeIndirect(VTPageTableResult LightmapVTPageTableResult, FVertexFactoryInterpolantsVSToPS Interpolants, float3 DiffuseDir, FMobileShadingModelContext ShadingModelContext, out half IndirectIrradiance, out half3 Color)
    {
        //To keep IndirectLightingCache conherence with PC, initialize the IndirectIrradiance to zero.
        IndirectIrradiance = 0;
        Color = 0;
    
        // 非直接漫反射.
    #if LQ_TEXTURE_LIGHTMAP
        float2 LightmapUV0, LightmapUV1;
        uint LightmapDataIndex;
        GetLightMapCoordinates(Interpolants, LightmapUV0, LightmapUV1, LightmapDataIndex);
    
        half4 LightmapColor = GetLightMapColorLQ(LightmapVTPageTableResult, LightmapUV0, LightmapUV1, LightmapDataIndex, DiffuseDir);
        Color += LightmapColor.rgb * ShadingModelContext.DiffuseColor * View.IndirectLightingColorScale;
        IndirectIrradiance = LightmapColor.a;
    #elif CACHED_POINT_INDIRECT_LIGHTING
        #if MATERIALBLENDING_MASKED || MATERIALBLENDING_SOLID
            // 將法線應用到半透明物體.
            FThreeBandSHVectorRGB PointIndirectLighting;
            PointIndirectLighting.R.V0 = IndirectLightingCache.IndirectLightingSHCoefficients0[0];
            PointIndirectLighting.R.V1 = IndirectLightingCache.IndirectLightingSHCoefficients1[0];
            PointIndirectLighting.R.V2 = IndirectLightingCache.IndirectLightingSHCoefficients2[0];
    
            PointIndirectLighting.G.V0 = IndirectLightingCache.IndirectLightingSHCoefficients0[1];
            PointIndirectLighting.G.V1 = IndirectLightingCache.IndirectLightingSHCoefficients1[1];
            PointIndirectLighting.G.V2 = IndirectLightingCache.IndirectLightingSHCoefficients2[1];
    
            PointIndirectLighting.B.V0 = IndirectLightingCache.IndirectLightingSHCoefficients0[2];
            PointIndirectLighting.B.V1 = IndirectLightingCache.IndirectLightingSHCoefficients1[2];
            PointIndirectLighting.B.V2 = IndirectLightingCache.IndirectLightingSHCoefficients2[2];
    
            FThreeBandSHVector DiffuseTransferSH = CalcDiffuseTransferSH3(DiffuseDir, 1);
    
            // 計算加入了法線影響的漫反射光照.
            half3 DiffuseGI = max(half3(0, 0, 0), DotSH3(PointIndirectLighting, DiffuseTransferSH));
    
            IndirectIrradiance = Luminance(DiffuseGI);
            Color += ShadingModelContext.DiffuseColor * DiffuseGI * View.IndirectLightingColorScale;
        #else 
            // 半透明使用無方向(Non-directional), 漫反射被打包在xyz, 已經在cpu端除了PI和SH漫反射.
            half3 PointIndirectLighting = IndirectLightingCache.IndirectLightingSHSingleCoefficient.rgb;
            half3 DiffuseGI = PointIndirectLighting;
    
            IndirectIrradiance = Luminance(DiffuseGI);
            Color += ShadingModelContext.DiffuseColor * DiffuseGI * View.IndirectLightingColorScale;
        #endif
    #endif
    
        return IndirectIrradiance;
    }
    
    // PS主入口.
    PIXELSHADER_EARLYDEPTHSTENCIL
    void Main( 
        FVertexFactoryInterpolantsVSToPS Interpolants
        , FMobileBasePassInterpolantsVSToPS BasePassInterpolants
        , in float4 SvPosition : SV_Position
        OPTIONAL_IsFrontFace
        , out half4 OutColor    : SV_Target0
    #if DEFERRED_SHADING_PATH
        , out half4 OutGBufferA    : SV_Target1
        , out half4 OutGBufferB    : SV_Target2
        , out half4 OutGBufferC    : SV_Target3
    #endif
    #if USE_SCENE_DEPTH_AUX
        , out float OutSceneDepthAux : SV_Target4
    #endif
    #if OUTPUT_PIXEL_DEPTH_OFFSET
        , out float OutDepth : SV_Depth
    #endif
        )
    {
    #if MOBILE_MULTI_VIEW
        ResolvedView = ResolveView(uint(BasePassInterpolants.MultiViewId));
    #else
        ResolvedView = ResolveView();
    #endif
    
    #if USE_PS_CLIP_PLANE
        clip(BasePassInterpolants.OutClipDistance);
    #endif
    
        // 解壓打包的插值數據.
    #if PACK_INTERPOLANTS
        float4 PackedInterpolants[NUM_VF_PACKED_INTERPOLANTS];
        VertexFactoryUnpackInterpolants(Interpolants, PackedInterpolants);
    #endif
    
    #if COMPILER_GLSL_ES3_1 && !OUTPUT_MOBILE_HDR && !MOBILE_EMULATION
        // LDR Mobile needs screen vertical flipped
        SvPosition.y = ResolvedView.BufferSizeAndInvSize.y - SvPosition.y - 1;
    #endif
    
        // 獲取材質的像素屬性.
        FMaterialPixelParameters MaterialParameters = GetMaterialPixelParameters(Interpolants, SvPosition);
        FPixelMaterialInputs PixelMaterialInputs;
        {
            float4 ScreenPosition = SvPositionToResolvedScreenPosition(SvPosition);
            float3 WorldPosition = BasePassInterpolants.PixelPosition.xyz;
            float3 WorldPositionExcludingWPO = BasePassInterpolants.PixelPosition.xyz;
            #if USE_WORLD_POSITION_EXCLUDING_SHADER_OFFSETS
                WorldPositionExcludingWPO = BasePassInterpolants.PixelPositionExcludingWPO;
            #endif
            CalcMaterialParametersEx(MaterialParameters, PixelMaterialInputs, SvPosition, ScreenPosition, bIsFrontFace, WorldPosition, WorldPositionExcludingWPO);
    
    #if FORCE_VERTEX_NORMAL
            // Quality level override of material's normal calculation, can be used to avoid normal map reads etc.
            MaterialParameters.WorldNormal = MaterialParameters.TangentToWorld[2];
            MaterialParameters.ReflectionVector = ReflectionAboutCustomWorldNormal(MaterialParameters, MaterialParameters.WorldNormal, false);
    #endif
        }
    
        // 像素深度偏移.
    #if OUTPUT_PIXEL_DEPTH_OFFSET
        ApplyPixelDepthOffsetForMobileBasePass(MaterialParameters, PixelMaterialInputs, OutDepth);
    #endif
          
        // Mask材質.
    #if !EARLY_Z_PASS_ONLY_MATERIAL_MASKING
        //Clip if the blend mode requires it.
        GetMaterialCoverageAndClipping(MaterialParameters, PixelMaterialInputs);
    #endif
    
        // 計算并緩存GBuffer數據, 防止后續多次采用紋理.
        FGBufferData GBuffer = (FGBufferData)0;
        GBuffer.WorldNormal = MaterialParameters.WorldNormal;
        GBuffer.BaseColor = GetMaterialBaseColor(PixelMaterialInputs);
        GBuffer.Metallic = GetMaterialMetallic(PixelMaterialInputs);
        GBuffer.Specular = GetMaterialSpecular(PixelMaterialInputs);
        GBuffer.Roughness = GetMaterialRoughness(PixelMaterialInputs);
        GBuffer.ShadingModelID = GetMaterialShadingModel(PixelMaterialInputs);
        half MaterialAO = GetMaterialAmbientOcclusion(PixelMaterialInputs);
    
        // 應用AO.
    #if APPLY_AO
        half4 GatheredAmbientOcclusion = Texture2DSample(AmbientOcclusionTexture, AmbientOcclusionSampler, SvPositionToBufferUV(SvPosition));
    
        MaterialAO *= GatheredAmbientOcclusion.r;
    #endif
    
        GBuffer.GBufferAO = MaterialAO;
    
        // 由于IEEE 754 (FP16)可表示的最小標準值是2^-24 = 5.96e-8, 而后面的粗糙度涉及到1.0 / Roughness^4的計算, 所以為了防止除零錯誤, 需保證Roughness^4 >= 5.96e-8, 此處直接Clamp粗糙度到0.015625(0.015625^4 = 5.96e-8).
        // 另外, 為了匹配PC端的延遲渲染(粗糙度存儲在8位的值), 因此也自動Clamp到1.0.
        GBuffer.Roughness = max(0.015625, GetMaterialRoughness(PixelMaterialInputs));
        
        // 初始化移動端著色模型上下文FMobileShadingModelContext.
        FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
        ShadingModelContext.Opacity = GetMaterialOpacity(PixelMaterialInputs);
    
        // 薄層透明度物
    #if MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT
        (......)
    #endif
    
        half3 Color = 0;
    
        // 自定義數據.
        half CustomData0 = GetMaterialCustomData0(MaterialParameters);
        half CustomData1 = GetMaterialCustomData1(MaterialParameters);
        InitShadingModelContext(ShadingModelContext, GBuffer, MaterialParameters.SvPosition, MaterialParameters.CameraVector, CustomData0, CustomData1);
        float3 DiffuseDir = MaterialParameters.WorldNormal;
    
        // 頭發模型.
    #if MATERIAL_SHADINGMODEL_HAIR
        (......)
    #endif
    
        // 光照圖虛擬紋理.
        VTPageTableResult LightmapVTPageTableResult = (VTPageTableResult)0.0f;
    #if LIGHTMAP_VT_ENABLED
        {
            float2 LightmapUV0, LightmapUV1;
            uint LightmapDataIndex;
            GetLightMapCoordinates(Interpolants, LightmapUV0, LightmapUV1, LightmapDataIndex);
            LightmapVTPageTableResult = LightmapGetVTSampleInfo(LightmapUV0, LightmapDataIndex, SvPosition.xy);
        }
    #endif
    
    #if LIGHTMAP_VT_ENABLED
        // This must occur after CalcMaterialParameters(), which is required to initialize the VT feedback mechanism
        // Lightmap request is always the first VT sample in the shader
        StoreVirtualTextureFeedback(MaterialParameters.VirtualTextureFeedback, 0, LightmapVTPageTableResult.PackedRequest);
    #endif
    
        // 計算非直接光.
        half IndirectIrradiance;
        half3 IndirectColor;
        ComputeIndirect(LightmapVTPageTableResult, Interpolants, DiffuseDir, ShadingModelContext, IndirectIrradiance, IndirectColor);
        Color += IndirectColor;
    
        // 預計算的陰影圖.
        half Shadow = GetPrimaryPrecomputedShadowMask(LightmapVTPageTableResult, Interpolants).r;
    
    #if DEFERRED_SHADING_PATH
        float4 OutGBufferD;
        float4 OutGBufferE;
        float4 OutGBufferF;
        float4 OutGBufferVelocity = 0;
    
        GBuffer.IndirectIrradiance = IndirectIrradiance;
        GBuffer.PrecomputedShadowFactors.r = Shadow;
    
        // 編碼GBuffer數據.
        EncodeGBuffer(GBuffer, OutGBufferA, OutGBufferB, OutGBufferC, OutGBufferD, OutGBufferE, OutGBufferF, OutGBufferVelocity);
    #else
    
    #if !MATERIAL_SHADINGMODEL_UNLIT
    
        // 天光.
    #if ENABLE_SKY_LIGHT
        half3 SkyDiffuseLighting = GetSkySHDiffuseSimple(MaterialParameters.WorldNormal);
        half3 DiffuseLookup = SkyDiffuseLighting * ResolvedView.SkyLightColor.rgb;
        IndirectIrradiance += Luminance(DiffuseLookup);
    #endif
                
        Color *= MaterialAO;
        IndirectIrradiance *= MaterialAO;
    
        float  ShadowPositionZ = 0;
    #if DIRECTIONAL_LIGHT_CSM && !MATERIAL_SHADINGMODEL_SINGLELAYERWATER
        // CSM陰影.
        if (UseCSM())
        {
            half ShadowMap = MobileDirectionalLightCSM(MaterialParameters.ScreenPosition.xy, MaterialParameters.ScreenPosition.w, ShadowPositionZ);
        #if ALLOW_STATIC_LIGHTING
            Shadow = min(ShadowMap, Shadow);
        #else
            Shadow = ShadowMap;
        #endif
        }
    #endif /* DIRECTIONAL_LIGHT_CSM */
    
        // 距離場陰影.
    #if APPLY_DISTANCE_FIELD
        if (ShadowPositionZ == 0)
        {
            Shadow = Texture2DSample(MobileBasePass.ScreenSpaceShadowMaskTexture, MobileBasePass.ScreenSpaceShadowMaskSampler, SvPositionToBufferUV(SvPosition)).x;
        }
    #endif
    
        half NoL = max(0, dot(MaterialParameters.WorldNormal, MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz));
        half3 H = normalize(MaterialParameters.CameraVector + MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz);
        half NoH = max(0, dot(MaterialParameters.WorldNormal, H));
    
        // 平行光 + IBL
    #if FULLY_ROUGH
        Color += (Shadow * NoL) * MobileDirectionalLight.DirectionalLightColor.rgb * ShadingModelContext.DiffuseColor;
    #else
        FMobileDirectLighting Lighting = MobileIntegrateBxDF(ShadingModelContext, GBuffer, NoL, MaterialParameters.CameraVector, H, NoH);
        // MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z保存了平行光的SpecularScale.
        Color += (Shadow) * MobileDirectionalLight.DirectionalLightColor.rgb * (Lighting.Diffuse + Lighting.Specular * MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z);
    
        // 頭發著色.
    #if    !(MATERIAL_SINGLE_SHADINGMODEL && MATERIAL_SHADINGMODEL_HAIR)
        (......)
    #endif
    #endif /* FULLY_ROUGH */
    
        // 局部光源, 最多4個.
    #if MAX_DYNAMIC_POINT_LIGHTS > 0 && !MATERIAL_SHADINGMODEL_SINGLELAYERWATER
    
            if(NumDynamicPointLights > 0)
            {
    
                #if SUPPORT_SPOTLIGHTS_SHADOW
                    FPCFSamplerSettings Settings;
                    Settings.ShadowDepthTexture = DynamicSpotLightShadowTexture;
                    Settings.ShadowDepthTextureSampler = DynamicSpotLightShadowSampler;
                    Settings.ShadowBufferSize = DynamicSpotLightShadowBufferSize;
                    Settings.bSubsurface = false;
                    Settings.bTreatMaxDepthUnshadowed = false;
                    Settings.DensityMulConstant = 0;
                    Settings.ProjectionDepthBiasParameters = 0;
                #endif
    
                AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);
            
                if (MAX_DYNAMIC_POINT_LIGHTS > 1 && NumDynamicPointLights > 1)
                {
                    AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);
    
                    if (MAX_DYNAMIC_POINT_LIGHTS > 2 && NumDynamicPointLights > 2)
                    {
                        AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);
    
                        if (MAX_DYNAMIC_POINT_LIGHTS > 3 && NumDynamicPointLights > 3)
                        {
                            AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);
                        }
                    }
                }
            }
    
    #endif
    
        // 天空光.
    #if ENABLE_SKY_LIGHT
        #if MATERIAL_TWOSIDED && LQ_TEXTURE_LIGHTMAP
        if (NoL == 0)
        {
        #endif
    
        #if MATERIAL_SHADINGMODEL_SINGLELAYERWATER
            ShadingModelContext.WaterDiffuseIndirectLuminance += SkyDiffuseLighting;
        #endif
            Color += SkyDiffuseLighting * half3(ResolvedView.SkyLightColor.rgb) * ShadingModelContext.DiffuseColor * MaterialAO;
        #if MATERIAL_TWOSIDED && LQ_TEXTURE_LIGHTMAP
        }
        #endif
    #endif
    
    #endif /* !MATERIAL_SHADINGMODEL_UNLIT */
    
    #if MATERIAL_SHADINGMODEL_SINGLELAYERWATER
        (......)
    #endif // MATERIAL_SHADINGMODEL_SINGLELAYERWATER
    
    #endif// DEFERRED_SHADING_PATH
    
        // 處理頂點霧.
        half4 VertexFog = half4(0, 0, 0, 1);
    #if USE_VERTEX_FOG
    #if PACK_INTERPOLANTS
        VertexFog = PackedInterpolants[0];
    #else
        VertexFog = BasePassInterpolants.VertexFog;
    #endif
    #endif
        
        // 自發光.
        half3 Emissive = GetMaterialEmissive(PixelMaterialInputs);
    #if MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT
        Emissive *= TopMaterialCoverage;
    #endif
        Color += Emissive;
    
    #if !MATERIAL_SHADINGMODEL_UNLIT && MOBILE_EMULATION
        Color = lerp(Color, ShadingModelContext.DiffuseColor, ResolvedView.UnlitViewmodeMask);
    #endif
    
        // 組合霧顏色到輸出顏色.
        #if MATERIALBLENDING_ALPHACOMPOSITE || MATERIAL_SHADINGMODEL_SINGLELAYERWATER
            OutColor = half4(Color * VertexFog.a + VertexFog.rgb * ShadingModelContext.Opacity, ShadingModelContext.Opacity);
        #elif MATERIALBLENDING_ALPHAHOLDOUT
            // not implemented for holdout
            OutColor = half4(Color * VertexFog.a + VertexFog.rgb * ShadingModelContext.Opacity, ShadingModelContext.Opacity);
        #elif MATERIALBLENDING_TRANSLUCENT
            OutColor = half4(Color * VertexFog.a + VertexFog.rgb, ShadingModelContext.Opacity);
        #elif MATERIALBLENDING_ADDITIVE
            OutColor = half4(Color * (VertexFog.a * ShadingModelContext.Opacity.x), 0.0f);
        #elif MATERIALBLENDING_MODULATE
            half3 FoggedColor = lerp(half3(1, 1, 1), Color, VertexFog.aaa * VertexFog.aaa);
            OutColor = half4(FoggedColor, ShadingModelContext.Opacity);
        #else
            OutColor.rgb = Color * VertexFog.a + VertexFog.rgb;
    
            #if !MATERIAL_USE_ALPHA_TO_COVERAGE
                // Scene color alpha is not used yet so we set it to 1
                OutColor.a = 1.0;
    
                #if OUTPUT_MOBILE_HDR 
                    // Store depth in FP16 alpha. This depth value can be fetched during translucency or sampled in post-processing
                    OutColor.a = SvPosition.z;
                #endif
            #else
                half MaterialOpacityMask = GetMaterialMaskInputRaw(PixelMaterialInputs);
                OutColor.a = GetMaterialMask(PixelMaterialInputs) / max(abs(ddx(MaterialOpacityMask)) + abs(ddy(MaterialOpacityMask)), 0.0001f) + 0.5f;
            #endif
        #endif
    
        #if !MATERIALBLENDING_MODULATE && USE_PREEXPOSURE
            OutColor.rgb *= ResolvedView.PreExposure;
        #endif
    
        #if MATERIAL_IS_SKY
            OutColor.rgb = min(OutColor.rgb, Max10BitsFloat.xxx * 0.5f);
        #endif
    
    #if USE_SCENE_DEPTH_AUX
        OutSceneDepthAux = SvPosition.z;
    #endif
    
        // 處理顏色的alpha.
    #if USE_EDITOR_COMPOSITING && (MOBILE_EMULATION)
        // Editor primitive depth testing
        OutColor.a = 1.0;
        #if MATERIALBLENDING_MASKED
            // some material might have an opacity value
            OutColor.a = GetMaterialMaskInputRaw(PixelMaterialInputs);
        #endif
        clip(OutColor.a - GetMaterialOpacityMaskClipValue());
    #else
        #if OUTPUT_GAMMA_SPACE
            OutColor.rgb = sqrt(OutColor.rgb);
        #endif
    #endif
    
    #if NUM_VIRTUALTEXTURE_SAMPLES || LIGHTMAP_VT_ENABLED
        FinalizeVirtualTextureFeedback(
            MaterialParameters.VirtualTextureFeedback,
            MaterialParameters.SvPosition,
            ShadingModelContext.Opacity,
            View.FrameNumber,
            View.VTFeedbackBuffer
        );
    #endif
    }
    

    移動端的BasePassPS的處理過程比較復雜,步驟繁多,主要有:解壓插值數據,獲取并計算材質屬性,計算并緩村GBuffer,處理或調整GBuffer數據,計算前向渲染分支的光照(平行光、局部光),計算距離場、CSM等陰影,計算天空光,處理靜態光照、非直接光和IBL,計算霧效,以及處理水體、頭發、薄層透明度等特殊著色模型。

    由于標準16位浮點數(FP16)可表示的最小值是\(\cfrac{1.0}{2^{24}} = 5.96 \cdot 10^{-8}\),而后續的光照計算涉及到粗糙度的4次方運算(\(\cfrac{1.0}{\text{Roughness}^4}\)),為了防止除零錯誤,需要將粗糙度截取到\(0.015625\)\(0.015625^4 = 5.96 \cdot 10^{-8}\))。

    GBuffer.Roughness = max(0.015625, GetMaterialRoughness(PixelMaterialInputs));
    

    這也警示我們在開發移動端的渲染特性時,需要格外注意和把控數據精度,否則在低端設備經常由于數據精度不足而出現各種奇葩的畫面異常。

    雖然上面的代碼較多,但由很多宏控制著,實際渲染單個材質所需的代碼可能只是其中的很小的一個子集。比如說,默認支持4個局部光源,但如果在工程配置(下圖)中可以設為2或更少,則實際執行的光源指令少了很多。

    如果是前向渲染分支,則GBuffer的很多處理將被忽略;如果是延遲渲染分支,則平行光、局部光源的計算將被忽略,由延遲渲染Pass的shader執行。

    下面對重要接口EncodeGBuffer做剖析:

    void EncodeGBuffer(
        FGBufferData GBuffer,
        out float4 OutGBufferA,
        out float4 OutGBufferB,
        out float4 OutGBufferC,
        out float4 OutGBufferD,
        out float4 OutGBufferE,
        out float4 OutGBufferVelocity,
        float QuantizationBias = 0        // -0.5 to 0.5 random float. Used to bias quantization.
        )
    {
        if (GBuffer.ShadingModelID == SHADINGMODELID_UNLIT)
        {
            OutGBufferA = 0;
            SetGBufferForUnlit(OutGBufferB);
            OutGBufferC = 0;
            OutGBufferD = 0;
            OutGBufferE = 0;
        }
        else
        {
            // GBufferA: 八面體壓縮后的法線, 預計算陰影因子, 逐物體數據.
    #if MOBILE_DEFERRED_SHADING
            OutGBufferA.rg = UnitVectorToOctahedron( normalize(GBuffer.WorldNormal) ) * 0.5f + 0.5f;
            OutGBufferA.b = GBuffer.PrecomputedShadowFactors.x;
            OutGBufferA.a = GBuffer.PerObjectGBufferData;
    #else
            (......)
    #endif
    
            // GBufferB: 金屬度, 高光度, 粗糙度, 著色模型, 其它Mask.
            OutGBufferB.r = GBuffer.Metallic;
            OutGBufferB.g = GBuffer.Specular;
            OutGBufferB.b = GBuffer.Roughness;
            OutGBufferB.a = EncodeShadingModelIdAndSelectiveOutputMask(GBuffer.ShadingModelID, GBuffer.SelectiveOutputMask);
    
            // GBufferC: 基礎色, AO或非直接光.
            OutGBufferC.rgb = EncodeBaseColor( GBuffer.BaseColor );
    
    #if ALLOW_STATIC_LIGHTING
            // No space for AO. Multiply IndirectIrradiance by AO instead of storing.
            OutGBufferC.a = EncodeIndirectIrradiance(GBuffer.IndirectIrradiance * GBuffer.GBufferAO) + QuantizationBias * (1.0 / 255.0);
    #else
            OutGBufferC.a = GBuffer.GBufferAO;
    #endif
    
            OutGBufferD = GBuffer.CustomData;
            OutGBufferE = GBuffer.PrecomputedShadowFactors;
        }
    
    #if WRITES_VELOCITY_TO_GBUFFER
        OutGBufferVelocity = GBuffer.Velocity;
    #else
        OutGBufferVelocity = 0;
    #endif
    }
    

    在默認光照模型(DefaultLit)下,BasePass輸出的結果有以下幾種紋理:

    12.3.3.3 MobileDeferredShading

    移動端延遲光照的VS和PC端是一樣的,都是DeferredLightVertexShaders.usf,但PS不一樣,用的是MobileDeferredShading.usf。由于VS和PC一樣,且沒有特殊的操作,此處就忽略,如果有興趣的同學可以看第五篇的小節5.5.3.1 DeferredLightVertexShader

    下面直接分析PS代碼:

    // Engine\Shaders\Private\MobileDeferredShading.usf
    
    (......)
    
    // 移動端光源數據結構體.
    struct FMobileLightData
    {
        float3 Position;
        float  InvRadius;
        float3 Color;
        float  FalloffExponent;
        float3 Direction;
        float2 SpotAngles;
        float SourceRadius;
        float SpecularScale;
        bool bInverseSquared;
        bool bSpotLight;
    };
    
    // 獲取GBuffer數據.
    void FetchGBuffer(in float2 UV, out float4 GBufferA, out float4 GBufferB, out float4 GBufferC, out float4 GBufferD, out float SceneDepth)
    {
        // Vulkan的子pass獲取數據.
    #if VULKAN_PROFILE
        GBufferA = VulkanSubpassFetch1(); 
        GBufferB = VulkanSubpassFetch2(); 
        GBufferC = VulkanSubpassFetch3(); 
        GBufferD = 0;
        SceneDepth = ConvertFromDeviceZ(VulkanSubpassDepthFetch());
        // Metal的子pass獲取數據.
    #elif METAL_PROFILE
        GBufferA = SubpassFetchRGBA_1(); 
        GBufferB = SubpassFetchRGBA_2(); 
        GBufferC = SubpassFetchRGBA_3(); 
        GBufferD = 0; 
        SceneDepth = ConvertFromDeviceZ(SubpassFetchR_4());
        // 其它平臺(DX, OpenGL)的子pass獲取數據.
    #else
        GBufferA = Texture2DSampleLevel(MobileSceneTextures.GBufferATexture, MobileSceneTextures.GBufferATextureSampler, UV, 0); 
        GBufferB = Texture2DSampleLevel(MobileSceneTextures.GBufferBTexture, MobileSceneTextures.GBufferBTextureSampler, UV, 0);
        GBufferC = Texture2DSampleLevel(MobileSceneTextures.GBufferCTexture, MobileSceneTextures.GBufferCTextureSampler, UV, 0);
        GBufferD = 0;
        SceneDepth = ConvertFromDeviceZ(Texture2DSampleLevel(MobileSceneTextures.SceneDepthTexture, MobileSceneTextures.SceneDepthTextureSampler, UV, 0).r);
    #endif
    }
    
    // 解壓GBuffer數據.
    FGBufferData DecodeGBufferMobile(
        float4 InGBufferA,
        float4 InGBufferB,
        float4 InGBufferC,
        float4 InGBufferD)
    {
        FGBufferData GBuffer;
        GBuffer.WorldNormal = OctahedronToUnitVector( InGBufferA.xy * 2.0f - 1.0f );
        GBuffer.PrecomputedShadowFactors = InGBufferA.z;
        GBuffer.PerObjectGBufferData = InGBufferA.a;  
        GBuffer.Metallic    = InGBufferB.r;
        GBuffer.Specular    = InGBufferB.g;
        GBuffer.Roughness    = max(0.015625, InGBufferB.b);
        // Note: must match GetShadingModelId standalone function logic
        // Also Note: SimpleElementPixelShader directly sets SV_Target2 ( GBufferB ) to indicate unlit.
        // An update there will be required if this layout changes.
        GBuffer.ShadingModelID = DecodeShadingModelId(InGBufferB.a);
        GBuffer.SelectiveOutputMask = DecodeSelectiveOutputMask(InGBufferB.a);
        GBuffer.BaseColor = DecodeBaseColor(InGBufferC.rgb);
    #if ALLOW_STATIC_LIGHTING
        GBuffer.GBufferAO = 1;
        GBuffer.IndirectIrradiance = DecodeIndirectIrradiance(InGBufferC.a);
    #else
        GBuffer.GBufferAO = InGBufferC.a;
        GBuffer.IndirectIrradiance = 1;
    #endif
        GBuffer.CustomData = HasCustomGBufferData(GBuffer.ShadingModelID) ? InGBufferD : 0;
        return GBuffer;
    }
    
    // 直接光照.
    half3 GetDirectLighting(
        FMobileLightData LightData, 
        FMobileShadingModelContext ShadingModelContext, 
        FGBufferData GBuffer, 
        float3 WorldPosition, 
        half3 CameraVector)
    {
        half3 DirectLighting = 0;
        
        float3 ToLight = LightData.Position - WorldPosition;
        float DistanceSqr = dot(ToLight, ToLight);
        float3 L = ToLight * rsqrt(DistanceSqr);
        
        // 光源衰減.
        float Attenuation = 0.0;
        if (LightData.bInverseSquared)
        {
            // Sphere falloff (technically just 1/d2 but this avoids inf)
            Attenuation = 1.0f / (DistanceSqr + 1.0f);
            Attenuation *= Square(saturate(1 - Square(DistanceSqr * Square(LightData.InvRadius))));
        }
        else
        {
            Attenuation = RadialAttenuation(ToLight * LightData.InvRadius, LightData.FalloffExponent);
        }
    
        // 聚光燈衰減.
        if (LightData.bSpotLight)
        {
            Attenuation *= SpotAttenuation(L, -LightData.Direction, LightData.SpotAngles);
        }
        
        // 如果衰減不為0, 則計算直接光照.
        if (Attenuation > 0.0)
        {
            half3 H = normalize(CameraVector + L);
            half NoL = max(0.0, dot(GBuffer.WorldNormal, L));
            half NoH = max(0.0, dot(GBuffer.WorldNormal, H));
            FMobileDirectLighting Lighting = MobileIntegrateBxDF(ShadingModelContext, GBuffer, NoL, CameraVector, H, NoH);
            DirectLighting = (Lighting.Diffuse + Lighting.Specular * LightData.SpecularScale) * (LightData.Color * (1.0 / PI) * Attenuation);
        }
        return DirectLighting;
    }
    
    // 光照函數.
    half ComputeLightFunctionMultiplier(float3 WorldPosition);
    // 使用光網格添加局部光照, 不支持動態陰影, 因為需要逐光源陰影圖.
    half3 GetLightGridLocalLighting(const FCulledLightsGridData InLightGridData, ...);
    
    // 平行光的PS主入口.
    void MobileDirectLightPS(
        noperspective float4 UVAndScreenPos : TEXCOORD0, 
        float4 SvPosition : SV_POSITION, 
        out half4 OutColor : SV_Target0)
    {
        // 恢復(讀取)GBuffer數據.
        FGBufferData GBuffer = (FGBufferData)0;
        float SceneDepth = 0; 
        {
            float4 GBufferA = 0; 
            float4 GBufferB = 0; 
            float4 GBufferC = 0; 
            float4 GBufferD = 0;
            FetchGBuffer(UVAndScreenPos.xy, GBufferA, GBufferB, GBufferC, GBufferD, SceneDepth);
            GBuffer = DecodeGBufferMobile(GBufferA, GBufferB, GBufferC, GBufferD);
        }
        
        // 計算基礎向量.
        float2 ScreenPos = UVAndScreenPos.zw;
        float3 WorldPosition = mul(float4(ScreenPos * SceneDepth, SceneDepth, 1), View.ScreenToWorld).xyz;
        half3 CameraVector = normalize(View.WorldCameraOrigin - WorldPosition);
        half NoV = max(0, dot(GBuffer.WorldNormal, CameraVector));
        half3 ReflectionVector = GBuffer.WorldNormal * (NoV * 2.0) - CameraVector;
        
        half3 Color = 0;
        // Check movable light param to determine if we should be using precomputed shadows
        half Shadow = LightFunctionParameters2.z > 0.0f ? 1.0f : GBuffer.PrecomputedShadowFactors.r;
    
        // CSM陰影.
    #if APPLY_CSM
        float  ShadowPositionZ = 0;
        float4 ScreenPosition = SvPositionToScreenPosition(float4(SvPosition.xyz,SceneDepth));
        float ShadowMap = MobileDirectionalLightCSM(ScreenPosition.xy, SceneDepth, ShadowPositionZ);
        Shadow = min(ShadowMap, Shadow);
    #endif
    
        // 著色模型上下文.
        FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
        {
            half DielectricSpecular = 0.08 * GBuffer.Specular;
            ShadingModelContext.DiffuseColor = GBuffer.BaseColor - GBuffer.BaseColor * GBuffer.Metallic;    // 1 mad
            ShadingModelContext.SpecularColor = (DielectricSpecular - DielectricSpecular * GBuffer.Metallic) + GBuffer.BaseColor * GBuffer.Metallic;    // 2 mad
            // 計算環境的BRDF.
            ShadingModelContext.SpecularColor = GetEnvBRDF(ShadingModelContext.SpecularColor, GBuffer.Roughness, NoV);
        }
        
        // 局部光源.
        float2 LocalPosition = SvPosition.xy - View.ViewRectMin.xy;
        uint GridIndex = ComputeLightGridCellIndex(uint2(LocalPosition.x, LocalPosition.y), SceneDepth);
        // 分簇光源
    #if USE_CLUSTERED
        {
            const uint EyeIndex = 0;
            const FCulledLightsGridData CulledLightGridData = GetCulledLightsGrid(GridIndex, EyeIndex);
            Color += GetLightGridLocalLighting(CulledLightGridData, ShadingModelContext, GBuffer, WorldPosition, CameraVector, EyeIndex, 0);
        }
    #endif
                
        // 計算平行光.
        half NoL = max(0, dot(GBuffer.WorldNormal, MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz));
        half3 H = normalize(CameraVector + MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz);
        half NoH = max(0, dot(GBuffer.WorldNormal, H));
        FMobileDirectLighting Lighting;
        Lighting.Specular = ShadingModelContext.SpecularColor * CalcSpecular(GBuffer.Roughness, NoH);
        Lighting.Diffuse = ShadingModelContext.DiffuseColor;
        Color += (Shadow * NoL) * MobileDirectionalLight.DirectionalLightColor.rgb * (Lighting.Diffuse + Lighting.Specular * MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z);
    
        // 處理反射(IBL, 反射捕捉器).
    #if APPLY_REFLECTION
        uint NumCulledEntryIndex = (ForwardLightData.NumGridCells + GridIndex) * NUM_CULLED_LIGHTS_GRID_STRIDE;
        uint NumLocalReflectionCaptures = min(ForwardLightData.NumCulledLightsGrid[NumCulledEntryIndex + 0], ForwardLightData.NumReflectionCaptures);
        uint DataStartIndex = ForwardLightData.NumCulledLightsGrid[NumCulledEntryIndex + 1];
    
        float3 SpecularIBL = CompositeReflectionCapturesAndSkylight(
            1.0f,
            WorldPosition,
            ReflectionVector,//RayDirection,
            GBuffer.Roughness,
            GBuffer.IndirectIrradiance,
            1.0f,
            0.0f,
            NumLocalReflectionCaptures,
            DataStartIndex,
            0,
            true);
            
        Color += SpecularIBL * ShadingModelContext.SpecularColor;
    #elif APPLY_SKY_REFLECTION
        float SkyAverageBrightness = 1.0f;
        float3 SpecularIBL = GetSkyLightReflection(ReflectionVector, GBuffer.Roughness, SkyAverageBrightness);
        SpecularIBL *= ComputeMixingWeight(GBuffer.IndirectIrradiance, SkyAverageBrightness, GBuffer.Roughness);
        Color += SpecularIBL * ShadingModelContext.SpecularColor;
    #endif
        // 天空光漫反射.
        half3 SkyDiffuseLighting = GetSkySHDiffuseSimple(GBuffer.WorldNormal);
        Color+= SkyDiffuseLighting * half3(View.SkyLightColor.rgb) * ShadingModelContext.DiffuseColor * GBuffer.GBufferAO;
        half LightAttenuation = ComputeLightFunctionMultiplier(WorldPosition);
    
    #if USE_PREEXPOSURE
        // MobileHDR applies PreExposure in tonemapper
        LightAttenuation *= View.PreExposure;    
    #endif
                        
        OutColor.rgb = Color.rgb * LightAttenuation;
        OutColor.a = 1;
    }
    
    // 局部光源的PS主入口.
    void MobileRadialLightPS(
        float4 InScreenPosition : TEXCOORD0,
        float4 SVPos            : SV_POSITION,
        out half4 OutColor        : SV_Target0
    )
    {
        FGBufferData GBuffer = (FGBufferData)0;
        float SceneDepth = 0; 
        {
            float2 ScreenUV = InScreenPosition.xy / InScreenPosition.w * View.ScreenPositionScaleBias.xy + View.ScreenPositionScaleBias.wz;
            float4 GBufferA = 0;  
            float4 GBufferB = 0; 
            float4 GBufferC = 0; 
            float4 GBufferD = 0;
            FetchGBuffer(ScreenUV, GBufferA, GBufferB, GBufferC, GBufferD, SceneDepth);
            GBuffer = DecodeGBufferMobile(GBufferA, GBufferB, GBufferC, GBufferD);
        }
        
        // With a perspective projection, the clip space position is NDC * Clip.w
        // With an orthographic projection, clip space is the same as NDC
        float2 ClipPosition = InScreenPosition.xy / InScreenPosition.w * (View.ViewToClip[3][3] < 1.0f ? SceneDepth : 1.0f);
        float3 WorldPosition = mul(float4(ClipPosition, SceneDepth, 1), View.ScreenToWorld).xyz;
        half3 CameraVector = normalize(View.WorldCameraOrigin - WorldPosition);
        half NoV = max(0, dot(GBuffer.WorldNormal, CameraVector));
        
        // 組裝光源數據結構體.
        FMobileLightData LightData = (FMobileLightData)0;
        {
            LightData.Position = DeferredLightUniforms.Position;
            LightData.InvRadius = DeferredLightUniforms.InvRadius;
            LightData.Color = DeferredLightUniforms.Color;
            LightData.FalloffExponent = DeferredLightUniforms.FalloffExponent;
            LightData.Direction = DeferredLightUniforms.Direction;
            LightData.SpotAngles = DeferredLightUniforms.SpotAngles;
            LightData.SpecularScale = 1.0;
            LightData.bInverseSquared = INVERSE_SQUARED_FALLOFF; 
            LightData.bSpotLight = IS_SPOT_LIGHT; 
        }
    
        FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
        {
            half DielectricSpecular = 0.08 * GBuffer.Specular;
            ShadingModelContext.DiffuseColor = GBuffer.BaseColor - GBuffer.BaseColor * GBuffer.Metallic;    // 1 mad
            ShadingModelContext.SpecularColor = (DielectricSpecular - DielectricSpecular * GBuffer.Metallic) + GBuffer.BaseColor * GBuffer.Metallic;    // 2 mad
            // 計算環境BRDF.
            ShadingModelContext.SpecularColor = GetEnvBRDF(ShadingModelContext.SpecularColor, GBuffer.Roughness, NoV);
        }
        
        // 計算直接光.
        half3 Color = GetDirectLighting(LightData, ShadingModelContext, GBuffer, WorldPosition, CameraVector);
        
        // IES, 光照函數.
        half LightAttenuation = ComputeLightProfileMultiplier(WorldPosition, DeferredLightUniforms.Position, -DeferredLightUniforms.Direction, DeferredLightUniforms.Tangent);
        LightAttenuation*= ComputeLightFunctionMultiplier(WorldPosition);
    
    #if USE_PREEXPOSURE
        // MobileHDR applies PreExposure in tonemapper
        LightAttenuation*= View.PreExposure;    
    #endif
    
        OutColor.rgb = Color * LightAttenuation;
        OutColor.a = 1;
    }
    

    以上可知,平行光和局部光源的PS是不同的入口,主要是因為兩者的區別較大,平行光直接在主入口計算光照,附帶計算了反射(IBL、捕捉器)、天空光漫反射;而局部光源會構建一個光源結構體,進入直接光計算函數,最后處理局部光源特有的IES和光照函數。

    另外,獲取GBuffer時,采用了SubPass特有的讀取模式,不同的著色平臺有所不同:

    // Vulkan
    [[vk::input_attachment_index(1)]]
    SubpassInput<float4> GENERATED_SubpassFetchAttachment0;
    #define VulkanSubpassFetch0() GENERATED_SubpassFetchAttachment0.SubpassLoad()
    
    // Metal
    Texture2D<float4> gl_LastFragDataRGBA_1;
    #define SubpassFetchRGBA_1() gl_LastFragDataRGBA_1.Load(uint3(0, 0, 0), 0)
    
    // DX / OpenGL
    Texture2DSampleLevel(GBufferATexture, GBufferATextureSampler, UV, 0);
    

     

     

    團隊招員

    博主所在的團隊正在用UE4開發一種全新的沉浸式體驗產品,急需各路豪士一同加入,共謀宏圖大業。目前急招以下職位:

    • UE邏輯開發。
    • UE引擎程序。
    • UE圖形渲染。
    • TA(技術向、美術向)。

    要求:對技術有熱情,扎實的技術基礎,良好的溝通和合作能力,有UE使用經驗或移動端開發經驗者更佳。

    有意向或想了解更多的請添加博主微信:81079389(注明博客園求職),或者發簡歷到博主郵箱:81079389#qq.com(#換成@)。

    靜待各路英雄豪杰相會。

     

     

    特別說明

    • Part 1結束,Part 2的內容有:
      • 移動端渲染技術
      • 移動端優化技巧
    • 感謝所有參考文獻的作者,部分圖片來自參考文獻和網絡,侵刪。
    • 本系列文章為筆者原創,只發表在博客園上,歡迎分享本文鏈接,但未經同意,不允許轉載
    • 系列文章,未完待續,完整目錄請戳內容綱目
    • 系列文章,未完待續,完整目錄請戳內容綱目
    • 系列文章,未完待續,完整目錄請戳內容綱目

     

    參考文獻

    posted @ 2021-11-05 01:24  0向往0  閱讀(4455)  評論(5編輯  收藏  舉報
    国产美女a做受大片观看