1. 程式人生 > >剖析虛幻渲染體系(03)- 渲染機制

剖析虛幻渲染體系(03)- 渲染機制

[TOC]     # **3.1 本篇概述和基礎** ## **3.1.1 渲染機制概述** 本篇主要講述UE怎麼將場景的物體怎麼組織成一個個Draw Call,期間做了那些優化和處理以及場景渲染器是如何渲染整個場景的。主要涉及的內容有: * 模型繪製流程。 * 動態和靜態渲染路徑。 * 場景渲染器。 * 涉及的基礎概念和優化技術。 * 核心類和介面的程式碼剖析。 後面的章節會具體涉及這些技術。 ## **3.1.2 渲染機制基礎** 按慣例,為了更好地切入本篇主題,先闡述或回顧一下本篇將會涉及的一些基礎概念和型別。 | 型別 | 解析 | | ------------------------ | ------------------------------------------------------------ | | **UPrimitiveComponent** | 圖元元件,是所有可渲染或擁有物理模擬的物體父類。是CPU層裁剪的最小粒度單位。 | | **FPrimitiveSceneProxy** | 圖元場景代理,是UPrimitiveComponent在渲染器的代表,映象了UPrimitiveComponent在渲染執行緒的狀態。 | | **FPrimitiveSceneInfo** | 渲染器內部狀態(描述了FRendererModule的實現),相當於融合了UPrimitiveComponent and FPrimitiveSceneProxy。只存在渲染器模組,所以引擎模組無法感知到它的存在。 | | **FScene** | 是UWorld在渲染模組的代表。只有加入到FScene的物體才會被渲染器感知到。渲染執行緒擁有FScene的所有狀態(遊戲執行緒不可直接修改)。 | | **FSceneView** | 描述了FScene內的單個檢視(view),同個FScene允許有多個view,換言之,一個場景可以被多個view繪製,或者多個view同時被繪製。每一幀都會建立新的view例項。 | | **FViewInfo** | view在渲染器的內部代表,只存在渲染器模組,引擎模組不可見。 | | **FSceneRenderer** | 每幀都會被建立,封裝幀間臨時資料。下派生FDeferredShadingSceneRenderer(延遲著色場景渲染器)和FMobileSceneRenderer(移動端場景渲染器),分別代表PC和移動端的預設渲染器。 | | **FMeshBatchElement** | 單個網格模型的資料,包含網格渲染中所需的部分資料,如頂點、索引、UniformBuffer及各種標識等。 | | **FMeshBatch** | 存著一組FMeshBatchElement的資料,這組FMeshBatchElement的資料擁有相同的材質和頂點緩衝。 | | **FMeshDrawCommand** | 完整地描述了一個Pass Draw Call的所有狀態和資料,如shader繫結、頂點資料、索引資料、PSO快取等。 | | **FMeshPassProcessor** | 網格渲染Pass處理器,負責將場景中感興趣的網格物件執行處理,將其由FMeshBatch物件轉成一個或多個FMeshDrawCommand。 | 需要特意指出,以上概念中除了UPrimitiveComponent是屬於遊戲執行緒的物件,其它皆屬於渲染執行緒。   # **3.2 模型繪製管線** ## **3.2.1 模型繪製管線概覽** 在學習OpenGL或DirectX等圖形API時,想必大家肯定都接觸過類似的程式碼(以OpenGL畫三角形為例): ```c++ void DrawTriangle() { // 構造三角形頂點和索引資料. float vertices[] = { 0.5f, 0.5f, 0.0f, // top right 0.5f, -0.5f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, // bottom left -0.5f, 0.5f, 0.0f // top left }; unsigned int indices[] = { 0, 1, 3, // first Triangle 1, 2, 3 // second Triangle }; // 建立GPU側的資源並繫結. unsigned int VBO, VAO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); // 清理背景 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 繪製三角形 glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); } ``` 以上的Hello Triangle大致經過了幾個階段:構造CPU資源,建立和繫結GPU側資源,呼叫繪製介面。這對於簡單的應用程式,或者學習圖形學來言,直接呼叫圖形學API可以簡化過程,直奔主題。但是,對於商業遊戲引擎而言,需要以每秒數十幀渲染複雜的場景(成百上千個Draw Call,數十萬甚至數百萬個三角形),肯定不能直接採用簡單的圖形API呼叫。 商業遊戲引擎需要在真正呼叫圖形API之前,需要做很多操作和優化,諸如遮擋剔除、動態和靜態合拼、動態Instance、快取狀態和命令、生成中間指令再轉譯成圖形API指令等等。 在UE4.21之前,為了達到上述的目的,採用了網格渲染流程(Mesh Draw Pipeline),示意圖如下: ![](https://img2020.cnblogs.com/blog/1617944/202103/1617944-20210319203832841-1939790306.jpg) *UE4.21及之前版本的網格繪製流程。* 大致過程是渲染之時,渲染器會遍歷場景的所有經過了可見性測試的PrimitiveSceneProxy物件,利用其介面收集不同的FMeshBatch,然後在不同的渲染Pass中遍歷這些FMeshBatch,利用Pass對應的DrawingPolicy將其轉成RHI的命令列表,最後才會生成對應圖形API的指令,提交到GPU硬體中執行。 UE4.22在此基礎上,為了更好地做渲染優化,給網格渲染管線進行了一次比較大的重構,拋棄了低效率的DrawingPolicy,用PassMeshProcessor取而代之,在FMeshBatch和RHI命令之間增加了一個概念FMeshDrawCommand,以便更大程度更加可控地排序、快取、合併繪製指令: ![](https://img2020.cnblogs.com/blog/1617944/202103/1617944-20210319203846059-346871767.jpg) *UE4.22重構後新的網格繪製流程。增加了新的FMeshDrawCommand和FMeshPassProcessor等概念及操作。* 這樣做的目的主要有兩個: * 支援RTX的實時光線追蹤。光線追蹤需要遍歷整個場景的物體,要保留整個場景的shader資源。 * GPU驅動的渲染管線。包含GPU裁剪,所以CPU沒法知道每一幀的可見性,但又不能每幀建立整個場景的繪製指令,否則無法達成實時渲染。 為了達成上述的目的,重構後的管線採取了更多聚合快取措施,體現在: * 靜態圖元在加入場景時就建立繪製指令,然後快取。 * 允許RHI層做盡可能多的預處理。 * shader Binding Table Entry。 * Graphics Pipeline State。 * 避免靜態網格每幀都重建繪製指令。 重構了模型渲染管線之後,多數場景案例下,DepthPass和BasePass可以減少數倍的Draw Call數量,快取海量的命令: ![](https://img2020.cnblogs.com/blog/1617944/202103/1617944-20210319203908808-1155568886.jpg) *Fortnite的一個測試場景在新舊網格渲染管線下的渲染資料對比。可見在新的網格渲染流程下,Draw Call得到了大量的降低,命令快取數量也巨大。* 本節的後續章節就以重構後的網格繪製流程作為剖析物件。 ## **3.2.2 從FPrimitiveSceneProxy到FMeshBatch** 在上一篇中,已經解析過FPrimitiveSceneProxy是遊戲執行緒UPrimitiveComponent在渲染執行緒的映象資料。而FMeshBatch是本節才接觸的新概念,它它包含了繪製Pass所需的所有資訊,解耦了網格Pass和FPrimitiveSceneProxy,所以FPrimitiveSceneProxy並不知道會被哪些Pass繪製。 FMeshBatch和FMeshBatchElement的主要宣告如下: ```c++ // Engine\Source\Runtime\Engine\Public\MeshBatch.h // 網格批次元素, 儲存了FMeshBatch單個網格所需的資料. struct FMeshBatchElement { // 網格的UniformBuffer, 如果使用GPU Scene, 則需要為null. FRHIUniformBuffer* PrimitiveUniformBuffer; // 網格的UniformBuffer在CPU側的資料. const TUniformBuffer