剖析虛幻渲染體系(03)- 渲染機制
阿新 • • 發佈:2021-03-28
[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