通過Filament引擎(二) ——引擎的調用及接口層核心對象的介紹我們知道,要在項目中使用filament,首先我們需要構建出filament的Engine的對象,然后通過filament::Engine對象實例,來構建其他對象,組裝渲染場景,執行渲染操作等。那么filament::Engine的構建過程具體發生了什么事情呢?我們組裝的渲染場景,又是如何被渲染渲染出來的?在本篇博客中,我們進一步去了解下Filament引擎的內部是如何工作的。
一、Engine構建
我們通過Engine::Builder().build()
來創建filament::Engine對象,會調用到Engine* FEngine::create(Builder const& builder)
函數來創建FEngine對象。
在編譯時,如果開啟FILAMENT_SINGLE_THREADED宏,則filament不會開啟單獨的渲染線程,否則Engine.create的時候,會構建單獨的DriverThread,渲染工作在此線程中執行。出于渲染效率的考慮,我們在使用filament時,一般都是會采用異步渲染的方式。
在異步渲染的模式下,filament渲染的核心對象Driver,會在渲染線程中根據調用者配置和當前運行環境進行構建。構建成功后,通知工作線程Driver構建成功,然后渲染線程會進入命令處理的循環中。
二、工作線程
我們將調用Filament命令的線程稱為工作線程。在工作線程中,我們調用filament的API,創建IndexBuffer、VertexBufer、Texture、Material之類的渲染對象,構建渲染場景,然后調用渲染器進行渲染,實際上并不會真正的調用OpenGL、Metal、Vulkan、DirectX這樣的底層渲染API。
在構建渲染對象時,實際上會調用我們封裝的渲染驅動(OpenGLDriver、MetalDriver、VulkanDriver等,具體調用取決于平臺及構建Engine時的配置),會先創建出filament抽象出的對應的RHI對象,如果需要調到底層渲染API,引擎會通過CommandStream在CircularBuffer實例中構建出對應的Command,由渲染線程進行執行。
在Filament引擎(一) ——渲染框架設計中異步渲染的實現這部分,對此部分實現,也進行了分析和說明。它用了一些C++開發中的小技巧,在此也不再贅述。
渲染驅動指令的構建
以IndexBuffer::setBuffer
的調用為例,其堆棧如下:
CommandStream::updateIndexBuffer
函數是通過DriverAPI.inc中的宏DECL_DRIVER_API_N
聲明,DECL_DRIVER_API_N
會展開為DECL_DRIVER_API
,在CommandStream.h中引入DriverAPI.inc前對DECL_DRIVER_API
進行了定義:
#define DECL_DRIVER_API(methodName, paramsDecl, params) \inline void methodName(paramsDecl) { \DEBUG_COMMAND_BEGIN(methodName, false, params); \using Cmd = COMMAND_TYPE(methodName); \void* const p = allocateCommand(CommandBase::align(sizeof(Cmd))); \new(p) Cmd(mDispatcher.methodName##_, APPLY(std::move, params)); \DEBUG_COMMAND_END(methodName, false); \}
所以,實際上CommandStream::updateIndexBuffer的實現,宏展開后如下:
inline void updateIndexBuffer(filament::backend::Handle<filament::backend::HwIndexBuffer> ibh, filament::backend::BufferDescriptor && data, unsigned int byteOffset){ mDriver.debugCommandBegin(this, false, "updateIndexBuffer");using Cmd = CommandType<decltype(&Driver::updateIndexBuffer)>::Command<&Driver::updateIndexBuffer>;void* const p = allocateCommand(CommandBase::align(sizeof(Cmd)));new(p) Cmd(mDispatcher.updateIndexBuffer_, std::move(ibh), std::move(data), std::move(byteOffset));mDriver.debugCommandEnd(this, false, "updateIndexBuffer");
}
filament以DriverAPI中的函數作為模板參數,通過CommandType模板類及其內部模板類Command,將DriverAPI函數及調用傳入的參數封裝成CommandBase的子類對象,對象存儲在CircularBuffer中。這樣,渲染線程就能不關注渲染的具體指令,而是按照統一的調用方式進行執行。
三、渲染線程
渲染線程需要做的工作,只是不斷的從CircularBuffer中取出需要執行命令,讓Driver進行執行。
FEngine::execute
:通過調用mCommandBufferQueue.waitForCommands
,每次循環從引擎實例中的CommandBufferQueue實例內,取出當前的待執行的Commands(std::vector<CommandBufferQueue::Range>
), 然后遍歷的將Range所指向的內存Buffer,傳遞給CommandStream.execute
進行處理。
-
CommandBufferQueue::Range
記錄的只有begin和end兩個void*
指針,指向的是一組Commands的起止地址。CircularBuffer、Command及Range的關系示意如下:
-
mCommandBufferQueue.waitForCommands
內部在命令隊列為空或者在暫停渲染時,進入等待狀態,阻塞渲染線程的工作。
CommandStream.execute
: CommandStream封裝了代表渲染驅動的Driver,在execute方法中,會將FEngine.execute
傳遞進來的buffer(CommandBufferQueue::Range
) 轉換成Command(CommandBase*
)進行執行。Command執行(CommandBase.execute
)會返回下一個CommandBase對象的指針。
四、幀渲染
通過上面的分析,我們大致可以知道,在工作線程中,我們調用的filament的API,會直接或間接的轉換成渲染驅動命令,存儲在CircularBuffer中,由渲染線程進行消費。
對于VertexBuffer、IndexBuffer、Texture等對象的創建,filament會構建出對應渲染驅動命令。對于Material對象,則會在其實例化的時候(createInstance
),構建對應的渲染驅動命令。渲染Entity的構建,會構建“創建渲染圖元”的驅動命令,這些調用路徑都相對比較簡單且直觀。
這種工作線程+渲染線程的方式,是現代渲染引擎比較通用的實現方案。其實對于一個渲染引擎來說,更關鍵的是,渲染場景的組織如何去設計,以及在工作線程中如何將組織好的渲染場景轉換成渲染線程可以“無腦”執行的渲染指令。
在filament中,當我們組織好場景后,會調用Renderer.beginFrame
、Renderer.render
以及Renderer.endFrame
進行場景的“渲染”。這個過程,實際上就是將我們組織的渲染場景,變成一系列的渲染驅動命令,發送到渲染線程執行。
beginFrame
在beginFrame中,會利用FrameSkipper去動態控制渲染幀的跳過策略,在渲染負載過高時返回false。我們在使用時,當beginFrame返回false時,就不在調用Renderer.render
,避免因渲染延遲導致畫面卡頓。
除此之外,beginFrame中主要做的工作包括:
SwapChain::makeCurrent
: 交換鏈和渲染環境的上下文綁定,讓當前渲染線程知道要把圖畫到哪里,確保后續的渲染操作能正確顯示在指定的面上。CommandStream::tick
: 發個指令讓渲染線程執行需要在渲染線程執行的周期性的任務。這些任務一般是因為渲染后端實現時需要控制某些渲染指令的調用時機而發出的任務。CommandStream::beginFrame
: 發送beginFrame的渲染驅動命令。不同驅動(MetalDriver、OpenGLDriver、VulkanDriver等)處理不同。FrameInfoManager::beginFrame
: 幀信息收集,FrameInfoManager用于管理幀級別的渲染元數據和性能監控信息。FEngine.prepare
: 主要工作都是針對材質,包括將材質實例中的參數的修改同步到渲染管線,保證運行時修改的材質參數能被GPU使用。以及檢查著色器程序與材質參數一致性。
render
filament中每幀的渲染由View進行組織,場景(Scene)和相機(Camera)是其進行內容呈現的必要元素。render的核心調用棧為:FRenderer::render
->FRenderer::renderInternal
->FRenderer::renderJob
。
在render中,主要是對場景、相機的存在進行判斷,以及保證渲染前的flush操作。
在renderInternal中,會基于Engine中RenderPassArena
構建一個RootArenaScope
對象,在渲染構建幀圖的過程中,RenderPass
會被存儲到RenderPassArean
中。在renderInternal執行完成后,RootAreanScope
自動析構,析構時會把新加入到RenderPassArean
中的RenderPass
使用的內存進行回收。此外,renderInternal中,會為JobSystem
創建rootJob,并在函數執行完成前runAndWait(rootJob)
,以保證在此間所有的Job都在管控中切執行完成。
在renderJob中,最為核心的工作包括幀圖(FrameGraph)的構建、編譯和執行 以及 后處理(PostProcessManager)的設置,主要的流程大致為:
engine.getPostProcessManager().setFrameUniforms(driver, view.getFrameUniforms())
將后處理過程中需要用到的渲染統一變量進行同步。- 獲取view中的各種配置項和功能狀態,包括抗鋸齒、防抖動、后處理等等各種配置和狀態。
view.prepare
進行View的準備工作,其入參中的cameraInfo由view.computeCameraInfo
返回,并根據后處理、fass等設置,在必要時將渲染視口的尺寸調整為16的倍數,幫助優化內存分配和四邊形渲染。scene->prepare
收集渲染此場景所需的所有信息。工作流程:- 遍歷Entities,按照光源實體和渲染實體進行分類,存儲到不同的容器中。
- 根據光源實體和渲染實體的容器中實體個數,調整
mLightData
和mRenderableData
的容器大小,他們分別是光照和渲染的SoA(Structure of Arrays)數據。 - 發起任務到JobSystem中執行,填充光照和渲染的SoA。定向光源(
directional ligth
)需要單獨處理。
- 當場景中設置了POINT、FOCUSED_SPOT及SPOT類型的光源,會在任務系統(JobSystem)中運行
FView::prepareVisibleLights
,對設置進來的這些光源進行準備工作。- 進行必要的光照剔除,確定場景中那些光源是可見的。未被標記參與光照投影計算的(
lightCaster
)、與視椎不相交等情況下的光源,會比標記為不可見。可見光源會被排到SoA的前面來,便于渲染處理。 - 計算光源和相機之間的距離,按照離相機由近到遠對光源進行排序。按照源碼中注釋的解釋,這么做是未來方便后續構建光源樹。如果光源數量超過GPU緩沖區所能容納的數量,距離相機較遠的光源會被舍棄掉。
- 進行必要的光照剔除,確定場景中那些光源是可見的。未被標記參與光照投影計算的(
- 開啟了視椎裁切的時候,將不在視椎中的渲染對象進行剔除,標記為
VISIBLE_RENDERABLE_BIT
。 prepareVisibleLights
執行完成后,判斷如果存在動態光源,就進行Froxel化。Froxel是filament中的光源在視椎空間下的體素化,結合Frustum和Voxel造的詞。光照效果的渲染,filament采用的是分簇前向渲染的方式,來平衡渲染效果和渲染效率。關于filament具體的光照實現,在另外一篇博客中再進一步分析。- 進行陰影的準備工作,主要是根據光照信息進行ShadowMap的構建和更新。
- 按照渲染實體的可見性,將渲染的SoA進行分組,然后進行渲染對象的準備工作,更新渲染的SoA。分組包括:
- 主攝像機可見: 被主攝像機捕獲且需要直接渲染到屏幕的對象,會參與主渲染通道。
- 主攝像機可見且進行定向光陰影投射:主攝像機可見且需要為定向光投射陰影,在生成平行光陰影貼圖時渲染,同時也會參與主渲染通道。
- 進行定向光陰影投射:不可見于主攝像機,但需要進行定向光陰影投射,僅在生成平行光陰影貼圖時渲染。
- 潛在的點光源陰影投射:可能被點光源或聚光燈照射,但未被主攝像機直接看到,在生成點光源陰影貼圖時渲染。
- 明確不可見:完全不可見且無需參與任何渲染或陰影計算,會在渲染時被剔除。
- 進行光照信息的準備工作,更新光照的UBO,以及設置IBL(Indirect Light)等等。
view.prepareUpscaler
進行上采樣的設置。- FrameGraph的構建和設置,這部分在renderJob中占據最大的篇幅。其主要流程如下:
- 以Renderer中的
mResourceAllocator
作為入參,構建FrameGraph實例。 - 獲取FrameGraph中的Blackboard,它主要是作為FrameGraph中的全局資源管理器,用于存儲和傳遞渲染過程中需要共享的虛擬資源(如紋理、渲染目標等)。在有陰影效果時,設置其陰影資源,陰影資源會用到
view.prepare
時構建的ShadowMap。 - 構建FrameGraph的RenderTarget資源,作為FrameGraph的渲染目標。
- 根據渲染對象的可見性進行圖元更新。
- 在此過程中,會解析各種設置和狀態,構建RenderPass,作為FrameGraph中的渲染單元。
- 以Renderer中的
- FrameGraph的“編譯”和執行。編譯階段,會分析渲染通道依賴關系,進行無效節點的剔除,并標記資源生命周期,以保證在資源不再被使用時及時銷毀或回收。執行階段就是根據編譯結果動態實例化GPU資源并將渲染指令提交到真正的渲染線程中。
關于renderJob中關鍵源碼的具體分析,后續進行進一步的展開,此處僅做簡單的流程性分析。
endFrame
一幀渲染完結,由endFrame中來進行必要的指令提交,資源回收等工作,主要包括:
SwapChain::commit
: 交換鏈提交,一般會刷新緩沖區,可以看做是將渲染管線的后緩沖區提交到渲染鏈前段,使渲染結果能從GPU到顯示設備。FrameInfoManager::endFrame
: 幀信息收集完結,同beginFrame中的FrameInfoManager::beginFrame
對應。CommandStream::tick
: 發個指令讓渲染線程執行需要在渲染線程執行的周期性的任務。mResourceAllocator.gc
幀資源回收
歡迎轉載,轉載請保留文章出處。求閑的博客[https://blog.csdn.net/junzia/article/details/149294146]