filament是谷歌開源的一個基于物理渲染(PBR)的輕量級、高性能的實時渲染框架,其框架架構設計并不復雜,后端RHI的設計也比較簡單。重點其實在于項目中材質、光照模型背后的方程式和理論,以及對它們的實現。相關的信息,可以參考官方給出的文檔。filament比較注重運行效率,在實現上也使用了一些抽象和技巧,這些也是比較有意思的代碼,可以學習和借鑒。
框架的整體設計
按照github項目中的說明,filament是適用主流平臺的實時物理渲染引擎,設計的目的是期望在Android上小而快。
Filament is a real-time physically based rendering engine for Android, iOS, Linux, macOS, Windows, and WebGL. It is designed to be as small as possible and as efficient as possible on Android.
在其設計中,使用Entity、components下各Manager中用來表示Component的數據結構及Manager本身,再加上Renderer,形成了ECS(Entity-Component-System)架構。
另外filament中存在一個backend的子工程,是一套自定義的RHI(Render Hardware Interface),封裝了諸如OpenGL、Vulkan、Metal的后端渲染API(PS:沒有DirectX,Windows平臺,官方默認使用Vulkan)。
在“ECS”和“RHI”之間,filament通過Renderer類,內部使用RenderPass
、FrameGraph
等來組織backend提供的RHI進行渲染,承擔最重要的“RenderSystem”的工作。
渲染后端RHI設計
Filament的渲染后端RHI非常輕量,并沒有什么復雜的設計,使用起來也相對比較簡單。不過它在異步渲染的實現上有一些不太好看但是實用的“奇淫巧技”。
Filament定義了一個RHI空對象基類HwBase,然后派生了一系列的RHI對象,包括HwVertexBuffer
、HwIndexBuffer
、HwProgram
、HwTexture
、HwRenderTarget
等等。基本上和其他諸多渲染框架采用的是類似的概念,然后不同的后端渲染(OpenGL/Vulkan/Metal)繼承這些對象,進行了不同后端的實現,如基于HwProgram
派生了OpenGLProgram
、VulkanProgram
以及MetalProgram
。其他各類對象也是類似。
另外Filament有一個Driver的基類,基于這個Driver基類,派生了各后端渲染的Driver,包括VulkanDriver/MetalDriver/OpenGLDriver/NoopDriver,沒有DirectXDriver,Windows平臺,官方使用Vulkan來進行渲染。Driver作為渲染的主要入口,所有RHI對象的創建銷毀及更新,都經由Driver來進行調用。
RHI的使用流程
Filament的RHI抽象和封裝度并不復雜,所以在使用上,如果有用過OpenGL、Vulkan、Metal的API,那么理解Filament的后端渲染也比較簡單。主要的使用流程參考以下代碼:
// 1. 初始化RHI的Driver
auto backend = filament::Backend::METAL;
auto platform = PlatformFactory::create(&backend);
Platform::DriverConfig const driverConfig;
auto driver = platform->createDriver(nullptr, driverConfig);
auto api = driver;
// 2. 創建SwapChainHandle,作為輸出。如果需要輸出到Window上,需要利用Window指針來進行創建
// api.createSwapChain(view.ptr, 0)
auto swapChain = api.createSwapChainHeadless(256, 256, 0);
api.makeCurrent(swapChain, swapChain);
// 3. 創建ProgramHandle,后續用來進行渲染,依賴Program對象,注意,Program和ProgramHandle不是同一個東西,Program就是用來創建ProgramHandle的一個參數集合
Program progCfg; // 進行相關配置
progCfg.shaderLanguage(ShaderLanguage::MSL);
projCfg.shader(ShaderStage::VERTEX, mVertexBlob.data(), mVertexBlob.size());
projCfg.shader(ShaderStage::FRAGMENT, mFragmentBlob.data(), mFragmentBlob.size());
projCfg.setSamplerGroup(0, ShaderStageFlags::ALL_SHADER_STAGE_FLAGS, psamplers, sizeof(psamplers) / sizeof(psamplers[0]));
auto program = api.createProgram(progCfg);
// 4. 創建渲染需要的紋理,更新紋理數據
auto tex = api.createTexture(SamplerType::SAMPLER_2D, 1, TextureFormat::RGBA8, 1, 512, 512, 4, usage);
PixelBufferDescriptor descriptor = createImage();
api.update3DImage(tex, 1, 0, 0, 0, 512, 512, 1, std::move(descriptor));
// 5. 設置采樣方式
SamplerGroup samplers(1);
SamplerParams sparams = {};
sparams.filterMag = SamplerMagFilter::LINEAR;
sparams.filterMin = SamplerMinFilter::LINEAR_MIPMAP_NEAREST;
samplers.setSampler(0, { tex, sparams });
auto sgroup = api.createSamplerGroup(samplers.getSize(), utils::FixedSizeString<32>("Test"));
api.updateSamplerGroup(sgroup, samplers.toBufferDescriptor(api));
api.bindSamplers(0, sgroup);
// 6. 開始一幀渲染,一個完整的渲染周期,可能由很多次渲染組成的一個渲染幀
api.beginFrame(0, 0, 0);
// 7. 創建RenderTarget,配置RenderPassParams,進而開啟RenderPass。RenderPass一般表示的是在一個渲染目標上進行的一系列渲染。
RenderPassParams params = {};
params.flags.clear = TargetBufferFlags::COLOR;
params.clearColor = {0.f, 0.f, 1.f, 1.f};
params.flags.discardStart = TargetBufferFlags::ALL;
params.flags.discardEnd = TargetBufferFlags::NONE;
params.viewport.height = 512;
params.viewport.width = 512;
auto renderTarget = api.createDefaultRenderTarget(0);
api.beginRenderPass(renderTarget, params);
// 8. 配置PipelineState
PipelineState state;
state.program = program;
state.rasterState.colorWrite = true;
state.rasterState.depthWrite = false;
state.rasterState.depthFunc = RasterState::DepthFunc::A;
state.rasterState.culling = CullingMode::NONE;
// 9. 構建Primitive,Primitive主要包括頂點位置、頂點索引等相關數據
auto vertexBufferObj = api.createBufferObject(size, BufferObjectBinding::VERTEX, BufferUsage::STATIC);
auto vertexBufferInfo = api.createVertexBufferInfo(bufferCount, attributeCount, attributes);
auto vertexBuffer = api.createVertexBuffer(vertexBufferObj, vertexBufferInfo);
auto indexBuffer = api.createIndexBuffer(elementType, mIndexCount, BufferUsage::STATIC);
api.updateIndexBuffer(indexBuffer, std::move(indexBufferDesc), 0);
auto primitive = api.createRenderPrimitive( vertexBuffer, indexBuffer, PrimitiveType::TRIANGLES);
// 10. 渲染指令
api.draw(state, primitive, 0, mIndexCount, 1);
// 11. 終止RenderPass。
api.endRenderPass();
// 12. 一幀內要進行的所有渲染指令調用完后,提交并結束一幀渲染
api.commit(swapChain);
api.endFrame()
// 13. 如果當前的渲染任務結束了,不會再執行了,銷毀所有資源。
api.destroySamplerGroup(sgroup);
api.destroyProgram(program);
api.destroySwapChain(swapChain);
api.destroyRenderTarget(renderTarget);
// 14. 如果所有渲染結束,要終止渲染了,結束渲染
api.finish();
driver->purge();
driver->terminate();
以上是簡化的一個渲染流程中,在實際的渲染過程中,我們一般會進行多幀循環渲染。在一幀渲染過程中,我們也可能會有多個renderpass,甚至有subrenderpass。RenderTarget
、Program
、PipelineState
、Primitive
等RHI對象的創建,我們也不是一定要按照上面的順序來進行,只需要在被使用前創建好既可。
Driver中提供了startCapture
來進行CPU和GPU的監測,我們一般也并不需要用到。而且在實現上也只有MetalDriver調用了Metal的API進行了實現。需要使用時,在需要進行監測的區間,調用startCapture
/stopCapture
即可。
異步渲染的實現
以上的調用,主要是用來說明Filament的使用流程,但是在實際的應用中,考慮到渲染效率和對渲染幀率的要求,我們往往需要進行異步渲染,把CPU和G的操作盡量并行起來。這時候,所有Filament的渲染指令的調用,都需要被發送到另外的線程中進行執行。我們不能再直接使用Driver對象去進行相關渲染指令的調用,而應該用CommandStream
。
在Filament中有一個DriverAPI.inc的頭文件,采用宏定義的方式,定義了一系列的渲染API,宏的具體實現又由引入者來進行。
最終呈現的效果,就是Driver定義了一系列的Driver的API,各后端實現對其進行了繼承,并實現。
CommandStream沒有繼承Driver,但是通過引入DriverAPI.inc,實現了一套和Driver的API一一對應,可以將Driver的各命令提交到隊列中的方法。
DriverAPI.inc使用宏調用的方式定義了一系列的函數,宏的定義又并不是在DriverAPI.inc中,幾個宏DECL_DRIVER_API
、DECL_DRIVER_API_SYNCHRONOUS
以及DECL_DRIVER_API_RETURN
都是留給引用者定義,并在文件后undef這三個宏避免污染。每次引用DriverAPI.inc前,必須定義這三個宏,否則會編譯報錯。
Vulkan/Metal/OpenGL等各后端對于DECL_DRIVER_API
的定義并沒什么差別,就是直接聲明對應名稱的函數。只有NoopDriver,在定義DECL_DRIVER_API_RETURN
進行了空實現。
CommandStream/Dispatcher也都引入了DriverAPI.inc。
Dispatcher中DECL_DRIVER_API
的定義就是一個function,那么引入DriverAPI.inc實際上,就是定義了一堆的Function,這么做主要是為了幫助CommandStream實現DriverAPI.inc中的方法時,來進行Command的構建。
CommandStream中的DECL_DRIVER_API
的定義,是根據方法名(methodName),利用Dispatcher,構建出一個調用Driver.methodName的Command。這種實現方式雖然閱讀起來稍微麻煩一點,但是需要進行Driver函數的擴展會可以減少很多工作。
CommandStream
中需要的Command是通過模板的方式進行定義的,參考CommandType和Command模板,由Command自己存儲所有指令執行時所需要的參數信息。CommandStream
中有一個CircularBuffer
,用來存儲所有的Command,Cammand一般都是在CommandStream
調用DriverAPI.inc聲明的相關API時進行創建。創建過程是先根據Command對象需要的大小來申請內存,然后在這塊內存上構建(也可以說是初始化)Command對象。
在使用上,CommandStream
和Driver
具有基本一致的函數,利用Filament的backend去實現異步渲染時,相對同步渲染,只需要以下幾步:
- 創建
CommandBufferQueue
,然后利用Driver
實例和CommandBufferQueue
中的CircularBuffer
去構造一個CommandStream
。 - 創建一個渲染線程,在渲染線程中循環等待渲染指令,然后執行渲染指令。在必要的時候進行退出。
- 在另外的線程里面像使用
Driver
一樣,使用CommandStream
去執行相關命令接口。
當然,這是簡化的說明,在實際使用中肯定還需要做一些額外的處理工作。Filament在使用其backend時,基于backend的api進行了進一步的封裝,以簡化調用。
渲染系統的實現
在filament中,實際上是由Renderer類承擔了渲染系統的職責,其執行渲染工作的核心代碼比較簡單:
if (renderer->beginFrame(window->getSwapChain())) {for (filament::View* offscreenView: mOffscreenViews) {renderer->render(offscreenView);}for (auto const& view: window->mViews) {renderer->render(view->getView());}if (postRender) {postRender(mEngine, window->mViews[0]->getView(), mScene, renderer);}renderer->endFrame();
}
其主要使用流程為:
- beginFrame開啟一幀渲染。beginFrame的時候,需要指定一個SwapChain,決定了最終渲染輸出的位置。
- render執行一幀渲染。render執行一幀渲染的時候,不是直接就進行了渲染,而是構建FrameGraph,并進行compile,然后execute。
- endFrame結束一幀渲染
Renderer的每一次渲染,都是執行一次renderJob(這個函數比較復雜,理論上應該拆一下)。它進行的工作,主要就是構建一個FrameGraph,通過對其進行“編譯”,再執行,以達到優化渲染的目的。FrameGraph的構建看起來非常復雜,主要進行的工作是將外部設置的信息,轉換成FrameGraph中的ResourceNode
、PassNode
、VirtualResource
等列表,并在此過程中,構建DependencyGraph
,以明確PassNode
、ResourceNode
的依賴關系。
FrameGraph的“編譯”(compile
),主要做了以下事情:
- 遍歷它所包含的所有的
PassNode
。對于每個PassNode
, 會通過DependencyGraph
,去獲取它所依賴的資源,并把這些資源信息注冊到PassNode
當中。RendererTarget
的數據的更新在資源注冊后,通過調用PassNode.resolve
進行。在資源信息注冊到PassNode
時,每個資源會記錄并更新最早使用它的節點和最晚使用它的節點, 以便后續根據這個信息,來進行真實資源的創建和銷毀。 - 而后,資源列表會被遍歷,然后借助資源中記錄的最早和最晚使用它的節點信息,來把資源直接掛載到相應節點的資源構造(
Resource.devirtualize
)和資源析構列表(Resource.destroy
),這樣就后續就可以方便合理的進行資源的按需創建和銷毀。 - 遍歷所有的資源節點,解析它們的用途(
Resource.usage
),后續資源進行真實資源創建時(Resource.devirtualize
),需要用到。
FrameGraph的“執行”(execute
),也是遍歷它所包含的所有PassNode,針對每個PassNode進行一下工作:
- 資源準備,
VirtualResource.devirtualize
- RenderPassNode執行
- 根據RenderPassData列表,進行必要的RenderTarget的構造
- mPassBase->execute,將指令進行部分解析,轉換成RHI指令數據,加入到Engine下的CommandStream中,由CircularBuffer進行存儲和管理。
- 析構前面構造的RenderTarget
- 資源析構,
VirtualResource.destroy
需要注意的是,其中的資源構造和資源析構,并不一定是在一次循環里對同一個資源進行。而是根據需要,在資源不再被后續的PassNode使用時,才會被析構。
在異步模式下,filament的FEngine
構建時候,會啟動一個渲染線程,用來通過CommandStream
對應的CommandBuffferQueue
,來循環從CircularBuffer
中獲取指令列表,讓真正的Driver
來進行執行.執行完成后CircularBuffer
中相應的指令空間就會被回收,用來接受CommandStream
給的新指令。
ECS的實現
前面說到,Filament上層是一個ECS的設計。我們將其拆開來理解。引擎中的Entity結構非常簡單,它在內存中實際上就是一個unit32_t,代表的Entity的索引,它本身并不持有Component的數據,相對來說非常簡單,重點在于Component
和System
。
Filament中的Component更多的是一個概念,每類Component都有一個對應的Manager,Manager包含了:
- Component的數據結構
- 向Entity中增刪此Component。實際上Component的數據實例,是由Manager持有和管理,并記錄和Entity的關聯。
- 查詢某個Entity,是否具備Component。
- 更新Entity中對應Component的數據
Filament中的ComponentManager包括CameraManager
、LightManager
、RenderableManager
、TransformManager
。這些Manager的實現大同小異,都是依賴utils::SingleInstanceComponentManager
、utils::EntityInstance
這些模板類來做的實現。
以RenderableManager為例,其內部存在一個mManager成員,對象類型繼承自utils::SingleInstanceComponentManager
模板類,如下代碼所示。
using Base = utils::SingleInstanceComponentManager<Box, // AABBuint8_t, // LAYERSMorphWeights, // MORPH_WEIGHTSuint8_t, // CHANNELSInstancesInfo, // INSTANCESVisibility, // VISIBILITYutils::Slice<FRenderPrimitive>, // PRIMITIVESBones, // BONESFMorphTargetBuffer* // MORPHTARGET_BUFFER
>;struct Sim : public Base {using Base::gc;using Base::swap;struct Proxy {// all of this gets inlinedUTILS_ALWAYS_INLINEProxy(Base& sim, utils::EntityInstanceBase::Type i) noexcept: aabb{ sim, i } { }union {// this specific usage of union is permitted. All fields are identicalField<AABB> aabb;Field<LAYERS> layers;Field<MORPH_WEIGHTS> morphWeights;Field<CHANNELS> channels;Field<INSTANCES> instances;Field<VISIBILITY> visibility;Field<PRIMITIVES> primitives;Field<BONES> bones;Field<MORPHTARGET_BUFFER> morphTargetBuffer;};};UTILS_ALWAYS_INLINE Proxy operator[](Instance i) noexcept {return { *this, i };}UTILS_ALWAYS_INLINE const Proxy operator[](Instance i) const noexcept {return { const_cast<Sim&>(*this), i };}
};Sim mManager;
RenderableManager對外的函數,最后基本上是對mManager的包裝。mManager用一個StructureOfArrays的模板類對象實例mData,來管理著當前Manager對應類別的所有Component數據集。上面SingleInstanceComponentManager模板傳入的參數,實際就構成了Component的數據結構。Sim定義了下標運算符,使mManager可以像數組一樣通過索引取得Component的代理對象,訪問Component數據。
但是,**在內存中實際存儲時,并不是按照Component的數據結構來存儲的,而是把相同的屬性的數據放到了一起。**StructureOfArrays中,mArray是一個std::tuple,存儲了所有屬性數據的起始地址。以FRenderableManager中mManager的數據存儲為例,圖示如下:
在Filament的ECS中,S其實主要就一個,渲染系統Renderer
,上面已對其渲染執行的過程進行了簡單的分析。其構建FrameGraph的過程比較復雜,涉及到諸多信息的處理,另外還包含一些View的操作,后面有時間再對其構建過程進行解讀。
歡迎轉載,轉載請保留文章出處。求閑的博客[https://blog.csdn.net/junzia/article/details/141300106]