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

前言

前文咱們完成一些在開始跨平臺渲染引擎之路前所須要的鋪墊工做中的一部分:基礎信息收集,而且在最後梳理出了一些開源引擎來做爲咱們接下來的研究對象,從這些大牛的成果中咱們能夠學習到不少成熟的實現方案和設計思路,這些一方面能幫助咱們快速成長,另外一方面能夠幫助咱們在真正開始實現引擎前制定一個符合咱們需求而且大方向上不出錯的設計方案。html

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

本篇文章咱們先分析 bgfx 這個項目,至少 bgfx 能夠用來作什麼、怎麼編譯之類的就很少作介紹了,官方文檔都有。c++

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

從問題出發

若是每一個引擎的研究咱們都逐行代碼地看下去,那麼要耗費較長的時間不說,並且收穫到也不必定都是咱們真正須要的,總體的效率就會顯得很是低下,每一個開源項目都有不少咱們能夠學習也有不少是我的開發/設計習慣所致的結果,所以在這裏咱們同樣和上一篇中同樣,帶着問題出發,先思考咱們想要從這個引擎中學到哪些東西。github

以我我的的角度出發,我但願搞清楚的如下幾個內容:算法

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

這些問題首先能夠幫助我瞭解一個優秀的開源引擎的使用方式是什麼樣子的,是否有經過什麼樣的巧妙設計來讓使用方用起來更加駕輕就熟;接下來可讓我學習到這個項目是如何作好各平臺適配的,以及時經過什麼樣的方式來切換各類渲染驅動的;以後即是 bgfx 是如何設計它的渲染流程的,後續本身設計方案和實現時能夠借鑑哪些內容;最後就是一些擴展性的需求是如何與核心渲染api進行協做的,是直接包含在模塊內部仍是以組件的方式來不斷迭代。後端

那麼就按照上面的問題順序,啓程!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 中繼續選擇一個進行分析,最後彙總這三篇文章的內容,並定下渲染引擎後續的框架、渲染流程等等內容,而後碼下見功夫。

相關文章
相關標籤/搜索