跨平臺渲染引擎之路:bgfx分析

格子林ll發表於2019-03-16

前言

前文我們完成一些在開始跨平臺渲染引擎之路前所需要的鋪墊工作中的一部分:基礎資訊收集,並且在最後梳理出了一些開源引擎來作為我們接下來的研究物件,從這些大牛的成果中我們可以學習到很多成熟的實現方案和設計思路,這些一方面能幫助我們快速成長,另一方面可以幫助我們在真正開始實現引擎前制定一個符合我們需求並且大方向上不出錯的設計方案。

工欲善其事,必先磨其器,一個完善而正確的設計方案可以在後面落地實現的過程中不斷地指導我們的開發方向,同時也避免了後續頻繁地大規模重構甚至重寫的噁心事情發生,因此這個前期預研並確定方案的步驟是至關重要而且必須的。

本篇文章我們先分析 bgfx 這個專案,至少 bgfx 可以用來做什麼、怎麼編譯之類的就不多做介紹了,官方文件都有。

Tips:該文章基於 bgfx 的 bd2bbc84ed90512e0534135b1fcd51d02ae75c69(SHA1值)提交進行分析

從問題出發

如果每個引擎的研究我們都逐行程式碼地看下去,那麼要耗費較長的時間不說,而且收穫到也不一定都是我們真正需要的,整體的效率就會顯得非常低下,每個開源專案都有很多我們可以學習也有很多是個人開發/設計習慣所致的結果,因此在這裡我們一樣和上一篇中一樣,帶著問題出發,先思考我們想要從這個引擎中學到哪些東西。

以我個人的角度出發,我希望搞清楚的以下幾個內容:

  • 簡單的使用流程是怎麼樣的
  • 主要的渲染流水線是怎麼樣的?有哪些比較核心的類?
  • 是如何實現切換渲染驅動的
  • 是否需要持有平臺資料?又是如何持有和使用的
  • 文字、多邊形的繪製是怎麼實現的?
  • 粒子、光照這些擴充套件的效果是直接包含在渲染框架內的嗎?在bgfx上怎麼實現的?
  • 框架的一些特點

這些問題首先可以幫助我瞭解一個優秀的開源引擎的使用方式是什麼樣子的,是否有通過什麼樣的巧妙設計來讓使用方用起來更加得心應手;接下來可以讓我學習到這個專案是如何做好各平臺適配的,以及時通過什麼樣的方式來切換各種渲染驅動的;之後便是 bgfx 是如何設計它的渲染流程的,後續自己設計方案和實現時可以借鑑哪些內容;最後就是一些擴充套件性的需求是如何與核心渲染api進行協作的,是直接包含在模組內部還是以元件的方式來不斷迭代。

那麼就按照上面的問題順序,啟程!

使用流程

在渲染流程上我們使用 bgfx從入門到沒有放棄 裡使用 FBO 渲染紋理,並顯示到螢幕上例子,這篇文章中主要是講的這個例子的使用流程,我會在這個例子裡面加上一些在後續引擎開發中需要關注的點的分析,比如PlatformData的作用和流程、Init包含哪些資料等等。

在該例子中我們可以大概列出以下的步驟:

  1. 初始化渲染平臺資訊
  2. 初始化 bgfx 資源
  3. 設定頂點座標,紋理座標
  4. 設定清屏色
  5. 載入紋理,shader,組裝成 program
  6. 建立 FBO,繫結紋理
  7. 渲染 FBO
  8. 渲染 FBO 結果紋理到螢幕
  9. 銷燬資源
  10. 銷燬 bgfx

初始化渲染平臺資訊

僅以OpenGL為例,有做過OpenGL開發的同學肯定知道,OpenGL的渲染跟其上下文環境息息相關,在某個沒有上下文環境的執行緒中執行渲染操作會導致沒有效果、黑屏等等問題,因此我們可以通過持有上層的GL檢視等資料資源,從而在必要時刻保證上下文環境的正確性,從而避免出現渲染問題。

那麼假設 bgfx 預設是使用的 OpenGL ES 來實現渲染的話,那麼上層的 view 是如何與底層的 Egl 繫結在一起的?要回答這個問題,我們得知道,OpenGL最終的渲染,都是渲染在一個 EGLSurface 中,這個 EGLSurface 的建立方式如下:

EGLSurface EGLAPIENTRY eglCreateWindowSurface (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);
複製程式碼

其中第三個引數 EGLNativeWindowType 就是和上層 view 掛鉤的,

對於 Android 平臺來說,不管上層用 NativeActity,還是 GlSurfaceView 還是 SurfaceView,都需要一個代表螢幕渲染緩衝區的類 surface 來建立一個 NativeWindow(EGLNativeWindowType),然後繫結到 EGLSurface 中。

// surface 來自於上層
ANativeWindow *mWindow  = ANativeWindow_fromSurface(env, surface);
bgfx::PlatformData pd;
pd.ndt = NULL;
pd.nwh = mWindow;
pd.context = NULL;
pd.backBuffer = NULL;
pd.backBufferDS = NULL;
bgfx::setPlatformData(pd); // 設定平臺資訊,繫結上層 view
複製程式碼

對於 iOS 平臺來說,最終渲染都需要使用到 UIView 的 CALayer,如果是使用 OpengGL 則返回 CAEAGLLayer,如果是使用 Metal 則返回 CAMetalLayer,而與 Android 相同需要構造 PlatformData,區別在於 pd.nwh 在 iOS 下需要傳 CAEAGLLayer 或者 CAMetalLayer。

PlatformData

首先看一下 PlatformData 的資料結構:

struct PlatformData
{
    PlatformData();
	// 展示的型別
    void* ndt;          //!< Native display type. 
    // 用於展示最終結果的視窗,Android平臺下是ANativeWindow,iOS平臺下是EAGLLayer或者是CAMetalLayer context,OSX平臺下是NSWindow
    void* nwh;          //!< Native window handle.
    void* context;      //!< GL context, or D3D device.
    void* backBuffer;   //!< GL backbuffer, or D3D render target view.
    void* backBufferDS; //!< Backbuffer depth/stencil.
};
複製程式碼

可以看到PlatformData把所有成員變數都宣告為 void* 型別以便接受各個平臺各型別的資料物件,而PlatformData通過 bgfx::setPlatformData 介面來進行設定,在 bgfx.cpp 中有一個全域性變數 g_platformData 持有平臺資料物件。

PlatformDataGL上下文等渲染過程中所需要的資料,在bgfx中各自平臺寫了各自平臺的渲染器,如:renderer_vk.cpp,renderer._mtl.mm等,在各自的渲染器中通過全域性變數 g_platformData 的資料進行型別強制的方式轉換成各自平臺需要的資料,如下:

m_device = (id<MTLDevice>)g_platformData.context;
複製程式碼

同樣的各自平臺也有各自平臺的上下文檔案,如:glcontext_eagl.mm,glcontext_egl.cpp等,獲取資料的方式同渲染器:

CAEAGLLayer* layer = (CAEAGLLayer*)g_platformData.nwh;
複製程式碼

在bgfx的Demo中初始化PlatformData時,發現除了nwh之外,其餘引數均賦值為NULL

bgfx中在各自平臺的上下文檔案中(如glcontext_eagl.h,glcontext_egl.h)定義了一個GlContext結構體,結構體中有一個m_context的成員變數也是用來儲存當前平臺的GL上下文的。

在呼叫 GlContext:Create 介面時會去取全域性變數 g_platformData 中的 context ,如果是空的(一般情況下為空),則建立各自平臺的上下文環境,並賦值給 m_context 變數,此外,還將m_context賦值給g_internalData.context ,這個 g_internalData 也是bgfx_p.h宣告的一個全域性類變數,型別為 InternalData。

在GlContext結構體定義了一個isValid函式來判斷上下文是否有效,內部實現是通過判斷m_context變數是否為空的方式來確定上下文環境是否有效。

初始化bgfx資源

bgfx初始化資源是用 bgfx::Init 儲存初始化資訊,通過 bgfx::init(init) 介面進行初始化。

bgfx::Init init;
// 選擇一個渲染後端,當設定為 RendererType::Enum::Count 的時候,系統將預設選擇一個平臺,可以設定Metal,OpenGL ES,Direct 等
init.type = bgfx::RendererType::Enum::Count;
// 設定供應商介面Vendor PCI ID,預設設定為0將選擇第一個裝置來顯示。
// #define BGFX_PCI_ID_NONE                UINT16_C(0x0000) //!< Autoselect adapter.
// #define BGFX_PCI_ID_SOFTWARE_RASTERIZER UINT16_C(0x0001) //!< Software rasterizer.
// #define BGFX_PCI_ID_AMD                 UINT16_C(0x1002) //!< AMD adapter.
// #define BGFX_PCI_ID_INTEL               UINT16_C(0x8086) //!< Intel adapter.
// #define BGFX_PCI_ID_NVIDIA              UINT16_C(0x10de) //!< nVidia adapter.
init.vendorId = 0;
// 設定解析度大小
init.resolution.width = m_width;
init.resolution.height = m_height;
// BGFX_RESET_VSYNC 其作用主要是讓顯示卡的運算和顯示器重新整理率一致以穩定輸出的畫面質量。
init.resolution.reset = BGFX_RESET_VSYNC;
bgfx::init(init);
複製程式碼

Init

bgfx 使用 Init 物件來儲存初始化資訊,如螢幕解析度、重新整理機制、渲染框架等:

struct Init
{
    Init();
	/// 設定渲染後端,當設定成 RendererType::Count 時會選擇一個當前平臺的預設渲染後端
	/// 具體可見:`bgfx::RendererType`
    RendererType::Enum type;

	/// 設定供應商介面Vendor PCI ID,設定為 BGFX_PCI_ID_NONE 將選擇第一個裝置來顯示。
    ///   - `BGFX_PCI_ID_NONE` - Autoselect adapter.
    ///   - `BGFX_PCI_ID_SOFTWARE_RASTERIZER` - Software rasterizer.
    ///   - `BGFX_PCI_ID_AMD` - AMD adapter.
    ///   - `BGFX_PCI_ID_INTEL` - Intel adapter.
    ///   - `BGFX_PCI_ID_NVIDIA` - nVidia adapter.
    uint16_t vendorId;

    /// Device id. If set to 0 it will select first device, or device with
    /// matching id.
    /// 暫未研究該引數具體用處
    uint16_t deviceId;

    bool debug;   //!< Enable device for debuging.
    bool profile; //!< Enable device for profiling.

    /// Platform data.
    PlatformData platformData;

    /// 設定離屏後緩衝的解析度大小並充值引數
    /// 見:bgfx::Resolution
    Resolution resolution;

    struct Limits
    {
    uint16_t maxEncoders;     //!< encoder 執行緒的最大數量.
    uint32_t transientVbSize; //!< Maximum transient vertex buffer size.
    uint32_t transientIbSize; //!< Maximum transient index buffer size.
    };

    Limits limits;

    /// 接收事件回撥的介面
    /// 見: `bgfx::CallbackI`
    CallbackI* callback;

    /// Custom allocator. When a custom allocator is not
    /// specified, bgfx uses the CRT allocator. Bgfx assumes
    /// custom allocator is thread safe.
    /// 暫未研究該引數具體用處
    bx::AllocatorI* allocator;
};
複製程式碼

這裡我們可以看到 Resolution 和離屏緩衝區有聯絡,簡單看了一下程式碼,推測是所有渲染都先渲染至離屏緩衝區中,在呼叫 bgfx::frame() 時再將前後屏緩衝區切換顯示渲染結果,不知道是否是為了可以用來做使用小的離屏尺寸,最後放大到檢視大尺寸之類的優化,需要進一步研究。

構建頂點座標、紋理座標

// 封裝頂點物件
struct PosColorVertex {
    // 頂點座標
    float m_x;
    float m_y;
    float m_z;
    // 紋理座標
    int16_t m_u;
    int16_t m_v;
    // 頂點描述物件
    static bgfx::VertexDecl ms_decl;

    static void init() {
        // 這句話的意思是位置資料裡面,前三個 Float 型別是作為頂點座標,後兩個 Int16 類的值作為紋理的座標
        ms_decl
	      .begin()
	      .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
	      .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Int16, true)
	      .end();
    };
};

// 這個地方要注意了,此時 FBO 的紋理座標 Android 和 iOS 都是採用左下角作為紋理座標原點,
// iOS 或者 Mac 平臺在渲染的時候,也是使用同樣的座標來渲染,但是 Android 平臺不一樣,
// Android 平臺在渲染紋理的時候,是採用左上角作為紋理座標來渲染的,
// 所以對於 Android 平臺來說,下面還需要一個渲染的座標 s_Android_render_Vertices1
static PosColorVertex s_fbo_Vertices[] =
        {
                {-1.0f,  1.0f,  0.0f,      0, 0x7fff},
                { 1.0f,  1.0f,  0.0f, 0x7fff, 0x7fff},
                {-1.0f, -1.0f,  0.0f,      0,      0},
                { 1.0f, -1.0f,  0.0f, 0x7fff,      0},
        };

// Android 平臺渲染的座標和紋理頂點,左上角為紋理原點
static PosColorVertex s_Android_render_Vertices1[] =
        {
                {-1.0f,  1.0f,  0.0f,      0,      0},
                { 1.0f,  1.0f,  0.0f, 0x7fff,      0},
                {-1.0f, -1.0f,  0.0f,      0, 0x7fff},
                { 1.0f, -1.0f,  0.0f, 0x7fff, 0x7fff},
        };

// 頂點繪製順序
static const uint16_t s_TriList[] =
        {
                0, 2, 1,
                1, 2, 3,
        };
複製程式碼

設定清屏色

// 設定清屏色,0或者1或者其他資料代表 view_id 的編號,這個view內部是個結構體,它封裝了一個渲染的範圍,清屏色,FBO 等等引數,用作最後渲染框架渲染的時候用
bgfx::setViewClear(0
	, BGFX_CLEAR_COLOR|BGFX_CLEAR_DEPTH
	, 0xffffffff
	, 1.0f
	, 0
);
bgfx::setViewClear(1
	, BGFX_CLEAR_COLOR|BGFX_CLEAR_DEPTH
	, 0xffffffff
	, 1.0f
	, 0
);
複製程式碼

設定清屏色時會需要設定一個 view_id,這個view內部是個結構體,它封裝了渲染的範圍,清屏色,FBO 等等引數,用作最後渲染框架渲染的時候用,可以設定不同view的清屏色。

而這些View是用一個大小固定的陣列來作為容器承載,因此view是有上限的,目前上限是256,同時這些配置的資訊都儲存在一個Context的結構體裡面。

載入紋理、Shader、Program

// FBO 頂點緩衝區 Handle
bgfx::VertexBufferHandle m_vbh;
// Android 渲染頂點緩衝區 Handle
bgfx::VertexBufferHandle m_vbh_Android_render;
// 頂點繪製順序緩衝 Handle
bgfx::IndexBufferHandle m_ibh;

// FBO 處理紋理效果相關 program
bgfx::ProgramHandle m_program;
// 輸入紋理,用作 FBO 處理效果
bgfx::TextureHandle m_texture;
// 紋理 handle
bgfx::UniformHandle s_textureHandle;

// 用於顯示的 program
bgfx::ProgramHandle m_display_program;
// 用於顯示的紋理,此時來自於 FBO 的結果
bgfx::UniformHandle s_display_tex_Handle;


// Create static vertex buffer.
m_vbh = bgfx::createVertexBuffer(
// Static data can be passed with bgfx::makeRef
bgfx::makeRef(s_fbo_Vertices, sizeof(s_fbo_Vertices)), ms_decl
);

// Create static vertex buffer.
m_vbh_Android_render = bgfx::createVertexBuffer(
// Static data can be passed with bgfx::makeRef
bgfx::makeRef(s_Android_render_Vertices1, sizeof(s_Android_render_Vertices1)), ms_decl
);

// Create static index buffer for triangle strip rendering.
m_ibh = bgfx::createIndexBuffer(
// Static data can be passed with bgfx::makeRef
bgfx::makeRef(s_TriList, sizeof(s_TriList))
);

// 從 shader 建立 program
m_program = loadProgram("vs_cubes", "fs_cubes");
// shader的uniform
s_textureHandle = bgfx::createUniform("s_texColor", bgfx::UniformType::Int1);
// 建立紋理
m_texture = loadTexture("/sdcard/test04.jpg");

// 建立顯示的 program
m_display_program = loadProgram("vs_cubes", "display_fs_cubes");
// 顯示 program 中待傳入的紋理
s_display_tex_Handle = bgfx::createUniform("display_texColor", bgfx::UniformType::Int1);

複製程式碼

Handle與makeRef

在 bgfx 中每個Buffer、紋理、Program等都會有一個對應的Handle物件,並且通過bgfx的 creatXXX 介面來建立,這些介面基本都需要又一個makeRef態方法建立的物件。

makeRef 會建立一個 Memory 資料,裡面主要儲存著又是 void* 型別的各種資料,以及資料大小等,方便後續的 createXXX 讀取資料建立對應Handle等物件。

bgfx 通過定義以下巨集來快速實現各種資料的Handle:

#define BGFX_HANDLE(_name)                                                           \
	struct _name { uint16_t idx; };                                                  \
	inline bool isValid(_name _handle) { return bgfx::kInvalidHandle != _handle.idx; }
複製程式碼

目前有看到宣告瞭以下的Handle:

BGFX_HANDLE(DynamicIndexBufferHandle)
BGFX_HANDLE(DynamicVertexBufferHandle)
BGFX_HANDLE(FrameBufferHandle)
BGFX_HANDLE(IndexBufferHandle)
BGFX_HANDLE(IndirectBufferHandle)
BGFX_HANDLE(OcclusionQueryHandle)
BGFX_HANDLE(ProgramHandle)
BGFX_HANDLE(ShaderHandle)
BGFX_HANDLE(TextureHandle)
BGFX_HANDLE(UniformHandle)
BGFX_HANDLE(VertexBufferHandle)
BGFX_HANDLE(VertexDeclHandle)
複製程式碼

建立FBO,繫結紋理

// 切記 bgfx 的 FBO 初始化要定義成BGFX_INVALID_HANDLE,不然要被坑
bgfx::FrameBufferHandle m_fbh = BGFX_INVALID_HANDLE,;
// 不設定成BGFX_INVALID_HANDLE的話,這裡第一次上來,isValid就會返回true
if (!bgfx::isValid(m_fbh)) {
	m_fbh = bgfx::createFrameBuffer((uint16_t)m_width, (uint16_t)m_height, bgfx::TextureFormat::Enum::BGRA8);
}
複製程式碼

渲染FBO

// 設定渲染視窗大小
bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height));
// 繫結 FBO 到 View_Id 為0的這個 View 上,開始渲染,渲染開始是 submit 方法呼叫後。
bgfx::setViewFrameBuffer(0, m_fbh);
bgfx::setState(BGFX_STATE_WRITE_RGB|BGFX_STATE_WRITE_A);
// 設定 FBO 需要的輸入紋理
bgfx::setTexture(0, s_textureHandle, m_texture);
bgfx::submit(0, m_program);
複製程式碼

該步驟同樣通過指定 view_id 進而將引數繫結到對應的檢視上,並且最後通過 bgfx::submit(view_id, program_handle) 來提交資料;

submit 介面通過Context內部的Encoder呼叫subitmit介面,Encoder是用來負責提交來自多個執行緒的渲染指令的,一個執行緒只會有一個Encoder,通過bgfx::begin來獲取,Encoder內部同時儲存了變換矩陣、座標buffer等等資訊的原始資料。

setViewFrameBuffer 將 FrameBufferHandle 賦值給了對應 View 物件的 m_fbh 成員變數,而 setState 最終是到 EncoderImpl 的 setState 介面中,光看程式碼感覺像是設定混合模式之類的,而且還會影響到透明度排序,這裡不太清楚具體的用處。

渲染FBO結果紋理到螢幕

// 渲染到螢幕的 view 需要主動將該 view 的 FBO 設定為 invalid,然後從 FBO 中拿出 attach 的紋理,設定到這次渲染需要的輸入引數中,然後顯示
bgfx::setVertexBuffer(0, m_vbh_Android_render);
bgfx::setIndexBuffer(ibh);
bgfx::setViewRect(1, 0, 0, uint16_t(m_width), uint16_t(m_height) );
bgfx::setViewFrameBuffer(1, BGFX_INVALID_HANDLE);
bgfx::setState(BGFX_STATE_WRITE_RGB|BGFX_STATE_WRITE_A);
bgfx::setTexture(1, s_display_tex_Handle, bgfx::getTexture(m_fbh));
bgfx::submit(1, m_display_program);

// 顯示到螢幕
bgfx::frame();
複製程式碼

該步驟額外設定了頂點座標,介面第一個引數為0,後續操作多了一個bgfx::frame()用於將結果顯示到螢幕的操作

銷燬資源

bgfx::destroy(m_ibh);
bgfx::destroy(m_vbh);
bgfx::destroy(m_program);
bgfx::destroy(m_texture);
bgfx::destroy(s_textureHandle);
bgfx::destroy(s_display_tex_Handle);
複製程式碼

通過bgfx::destroy刪除傳入的資料handle。

銷燬介面用多型的方式來銷燬多種Handle,內部最終還是寫入到 CommandBuffer 中。

銷燬bgfx

bgfx::shutdown();
複製程式碼

渲染流程

上面主要介紹了 bgfx的一個簡單的使用流程,並在這個使用流程中穿插了一些我們自己研究專案原始碼的收穫,接下來開始梳理一下bgfx的渲染流水線是什麼樣的,這裡以 bgfx 的 Cube 例子作為研究的Demo,以OpenGL作為渲染後端。

接下來是關於梳理渲染流程時的一些梳理路程以及一些點的總結,不感興趣的話可以直接跳過看流程圖。

一開始在大致瀏覽了一下 bgfx 的目錄/檔案結構時,發現了一個 renderer_gl.cpp 檔案,裡面定義了一個 biltRender 的介面(該介面內部呼叫了 glDrawElements 方法),通過斷點該介面發現渲染呼叫時在(MAC OS)entry_osx.mmrun 方法中,該方法會一直迴圈直到程式退出。

entry_osx.mm 在462行呼叫了 bgfx::renderFrame() 方法,逐級向下依次呼叫以下介面:

1. s_ctx->renderFrame(msecs);(bgfx.cpp 1396行)
2. m_renderCtx->submit(m_render, m_clearQuad, m_textVideoMemBlitter);(bgfx.cpp 2294行)
3. blit(this, _textVideoMemBlitter, _render->m_textVideoMem);(renderer_gl.cpp 7650行)_
4. _blit(_renderCtx, _blitter, *_mem);(renderer_gl.cpp 669行)
5. _renderCtx->blitRender(_blitter, numIndices);(renderer_gl.cpp 803行)
複製程式碼

但是這時候發現內部的glDrawElements沒有通過判斷,因此實際上並沒有被呼叫到。

改變一下策略,通過在run 方法處斷點,然後一路跟蹤下去,發現會走到 bgfx.cpp 的 renderFrame 的 2270 行的 rendererExecCommands ,該方法內部會先提交渲染前指令。該方法呼叫後,接下來呼叫 2294 行的m_renderCtx->submit(m_render, m_clearQuad, m_textVideoMemBlitter) 來提交渲染命令,繼續往下跟蹤,通過斷點所有的 glDrawArrays 以及 glDrawElements 呼叫,切到更簡單的 Hello World 的例子中,最終呼叫點在 renderer_gl.cpp 的 7349 行的

GL_CHECK(glDrawElementsInstanced(prim.m_type
										, numIndices
										, indexFormat
										, (void*)(uintptr_t)(draw.m_startIndex*indexSize)
										, draw.m_numInstances
										) );
複製程式碼

在上述介面2次呼叫後,第一次斷點排查的介面:

_renderCtx->blitRender(_blitter, numIndices);(renderer_gl.cpp 803行)
複製程式碼

內部的 glDrawElements 也會被呼叫,此後便是保持 2次+1次 的方式迴圈,這些和bgfx的設計有關係,暫時不去關心,目前只重點關注整體的渲染流程,類似於在一些場景下也會呼叫 glDrawArrays 而不是 glDrawElements ,但是這個例子裡面沒有。

執行完渲染後回到 bgfx.cpp 的 renderFrame 的 2300 行執行 rendererExecCommands 提交渲染後指令,一次渲染的流程差不多到這裡就結束了。

流程圖

image-20190315202714262

CommandBuffer

bgfx 的頂點資料等資訊的設定,都是先快取在類似 RenderDraw 之類的物件中,RenderDraw 這些物件又快取在對應的 Encoder 中,Encoder 又依附在 Context 上,最後渲染的時候將這些資訊一個個commit批量使用gl命令來進行實際的執行操作,這樣可以在這裡做batch等優化操作。

這些渲染命令分為在渲染前執行和渲染後執行兩種,統一由 CommandBuffer 來管理,bgfx 是在使用者呼叫createXXXBuffer (如建立頂點資料Buffer)之類介面呼叫時,間接呼叫 Context 的 createXXXBuffer,其內部以 Buffer 的型別(在 CommandBuffer 中用列舉定義了各種命令型別)來判斷是用前置命令還是後置命令(通過 getCommandBuffer 來獲取),然後使用不同的 CommandBuffer(前置為 m_submit->m_cmdPre,後置為 m_submit->m_cmdPost )來寫入資料,接下來在renderFrame的時候再從這兩個Buffer裡讀取此前寫入的資料,並呼叫對應驅動如 renderer_gl.cpp 下的 RendererContextI 來設定資料。

###Encoder

另外在 bgfx 用例裡面的 update() 重新整理介面中設定的渲染資料可以分為兩類,一類是和View相關的如 setViewRect 之類的,這類會直接通過 Context 的成員函式進行設定,另一類是setUniform等和View無關的,這類會通過 Context 的 Encoder 間接呼叫對應的介面,而這些介面呼叫又通過一個定義的巨集 BGFX_ENCODER 轉接到 EncoderImpl 上去(在專案中經常看到這樣類似的巨集),而 EncoderImpl 中大多就是將這些引數寫入一個個Buffer或者其內部資料成員中,最終在 renderer_gl.cpp 之類對應驅動的渲染器內部的 submit 等介面中,通過將資料儲存在 Frame 裡的 RenderItem 再裡面的 RenderDraw 等一系列物件中,並在該介面內部完成繫結。

而 EncoderImpl 儲存資料的 Frame 和 bgfx 傳遞給具體 Renderer(即 m_render ) 的 submit 的 Frame 是怎麼同步的呢,這裡其實在 Context::begin 的時候會呼叫 EncoderImpl::begin ,同時傳入其 m_submit 成員,而該成員在非多執行緒的情況下與 m_render 是同一個物件,而至於在多執行緒情況下目前還沒有研究到。

CommandBuffer / Encoder

以目前閱讀程式碼下來的收穫來看,CommandBuffer 主要用於建立 VertexBuffer、Shader等GL資源或者執行GL指令,而 Encoder 則用來記錄各種引數如變換矩陣等等,在渲染前通過 Frame 帶上這些資料繫結到著色器裡對應的變數上。

切換渲染驅動

這部分網上已經有同學研究過了,具體過程可見 bgfx入門練習1——切換圖形API驅動模式DX與OpenGL 以及 bgfx入門練習2——找出DX,OpenGL驅動切換實現原理 ,這裡列出一些摘要。

bgfx 首先在 demo 呼叫 init 的時候會去判斷使用什麼引擎,在 config.h 頭部寫了這些驅動的判定,如果什麼都沒定義,就怎麼怎麼樣之類的,這些驅動在一個叫 s_rendererCreator 的陣列中,搜尋這個陣列,來到bgfx.cpp的 rendererCreate()函式中,在 if(s_rendererCreator[ii].supported) 這句下斷點,跟了下,就知道寫了個評估演算法,score最高的是DX11。

bgfx_utils.cpp 中有一個 loadShader 的靜態方法,該函式內部有不同驅動的分支判斷,但是繼續跟又斷了。

檢視 bgfx 的 src 目錄,發現在 renderer 下面有 shader.cpp、shader_dxbc.cpp 等,如在DX驅動模式下, shader_dxbc.cpp 的 555 行看到 readString 方法,下斷點,成功斷下來,換到Opengl下無效,可以判斷此處應為DX轉換程式碼,順便BC應該是Byte Code的意思。

之後在堆疊裡向上找,找到一處多型呼叫 bgfx.cpp,2405 行 case CommandBuffer::CreateShader: 這個分支下面有一句 m_renderCtx->createShader(handle, mem); 就是這裡做了多型處理。

繼續跟可以發現,如果是 OGL,就直接在 renderer_gl.cpp 中做 m_id = glCreateShader(m_type); 從而建立GL Shader。如果是DX,從位元組碼判定,然後在 renderer_d3d11.cpp 中 CreateVertexShader CreatePixelShader CreateComputeShader 。包括後續的渲染等操作也是通過這樣的多型方式進行。

看到這裡就有一個感觸,策略模式真是一招鮮,吃遍天啊。

文字繪製

通過 freetype 等第三方庫提供支援,可支援載入 ttf 字型檔案。

font_manager.h 中看到了一個 GlyphInfo 的結構體,內部有關於 x、y 偏移等控制的成員變數,但是看介面定義只有載入文字生成對應GlyphInfo資料(在 Font Demo 中是通過 font_manager.cpp 的 523 行的 FontManager::preloadGlyph 介面),而沒有看到通過 GlyphInfo 去控制繪製的情況,我們通過手動修改 preloadGlyph 裡面的最終結果值,如 glyphInfo.advance_x ,可控制字與字之間x方向間距,因此可推斷支援簡單的文字排版操作,具體如何實現、在細節處如何流轉後續繼續跟蹤。

看了下其他如 ogre 等渲染庫也都有使用 freetype ,還需要再研究3D文字是否可實現,通過何種方式實現,而bgfx中 freetype 與 opengl 等渲染驅動如何互動也需要屆時進一步研究。

多邊形繪製

類似文字繪製,是通過 nanovg 輔助實現的,具體在 bgfx 中如何互動實現的也需要屆時進一步研究。

效果系統擴充套件

在Demo中已有看到延遲渲染、粒子系統、光照等效果或實現方案,因此可見 bgfx 也已支援這些常見功能,具體實現方案如粒子效果Demo是在 update() 介面中接入自己的 psUpdate/psRender 等渲染流程,因此目前來看這些額外的功能都是bgfx基於核心渲染API之上額外疊加的系統,類似於一個個的外掛掛靠在bgfx的核心渲染系統上,而bgfx的核心庫只提供基礎渲染api的封裝,而具體每一個效果在其上如何實現後續再進行研究。

其他

不管是從建立還是到後面的設定等流程中,bgfx 都會以一系列的 Handle 來傳遞這些資料,再簡單看了下 bgfx 的 src 目錄,裡面 glcontext_xxx 檔案分為了 egl、eagl、nsgl 以及 wgl 等平臺,用以通過各個平臺自己的方式來管理 Surface、Context 等物件。而上面提到過的 renderer_xxx 則用來實現各個不同驅動下的渲染邏輯,同時還有一些 shader_xxx 檔案,推測是用來適配特定驅動/驅動版本的著色器相關功能的。

bgfx 需要依賴於另外兩個庫才能執行,一個是 bimg 用來做影象的編解碼等工作,還有一個 bx 的基礎庫,用以提供執行緒、除錯等一系列的基礎工具,這些庫的核心程式碼基本都直接一股腦地放在倉庫的src目錄下,感興趣的可以去翻一翻。

總結

在本章中我們首先列舉了希望從 bgfx 這個專案中學到哪些內容;緊接著介紹了一個簡單的使用流程,並在各個節點中插入了對一些細節的分析和整理;然後我們依次完成了對渲染流程、切換渲染驅動、文字與多邊形繪製、效果系統擴充套件等方面或較詳細或簡單的分析。

通過上述的這些分析,我們在開篇中提到的幾個問題也基本都得到了解決,但是隻有一個專案心裡難免還是有些懷疑,是不是所有的渲染引擎都是這樣差不多的流程,有沒有更好的實現方式等等,因此在真正開始 coding 之前,還會在 ogreUrho3D 中繼續選擇一個進行分析,最後彙總這三篇文章的內容,並定下渲染引擎後續的框架、渲染流程等等內容,然後碼下見功夫。

相關文章