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

前言

前文我們完成一些在開始跨平臺渲染引擎之路前所需要的鋪墊工作中的一部分:基礎(chǔ)信息收集,并且在最后梳理出了一些開源引擎來作為我們接下來的研究對象,從這些大牛的成果中我們可以學(xué)習(xí)到很多成熟的實(shí)現(xiàn)方案和設(shè)計(jì)思路,這些一方面能幫助我們快速成長,另一方面可以幫助我們在真正開始實(shí)現(xiàn)引擎前制定一個(gè)符合我們需求并且大方向上不出錯(cuò)的設(shè)計(jì)方案。

工欲善其事,必先磨其器,一個(gè)完善而正確的設(shè)計(jì)方案可以在后面落地實(shí)現(xiàn)的過程中不斷地指導(dǎo)我們的開發(fā)方向,同時(shí)也避免了后續(xù)頻繁地大規(guī)模重構(gòu)甚至重寫的惡心事情發(fā)生,因此這個(gè)前期預(yù)研并確定方案的步驟是至關(guān)重要而且必須的。

本篇文章我們先分析 bgfx 這個(gè)項(xiàng)目,至少 bgfx 可以用來做什么、怎么編譯之類的就不多做介紹了,官方文檔都有。

Tips:該文章基于 bgfx 的 bd2bbc84ed90512e0534135b1fcd51d02ae75c69(SHA1值)提交進(jìn)行分析

從問題出發(fā)

如果每個(gè)引擎的研究我們都逐行代碼地看下去,那么要耗費(fèi)較長的時(shí)間不說,而且收獲到也不一定都是我們真正需要的,整體的效率就會顯得非常低下,每個(gè)開源項(xiàng)目都有很多我們可以學(xué)習(xí)也有很多是個(gè)人開發(fā)/設(shè)計(jì)習(xí)慣所致的結(jié)果,因此在這里我們一樣和上一篇中一樣,帶著問題出發(fā),先思考我們想要從這個(gè)引擎中學(xué)到哪些東西。

以我個(gè)人的角度出發(fā),我希望搞清楚的以下幾個(gè)內(nèi)容:

  • 簡單的使用流程是怎么樣的
  • 主要的渲染流水線是怎么樣的?有哪些比較核心的類?
  • 是如何實(shí)現(xiàn)切換渲染驅(qū)動的
  • 是否需要持有平臺數(shù)據(jù)?又是如何持有和使用的
  • 文字、多邊形的繪制是怎么實(shí)現(xiàn)的?
  • 粒子、光照這些擴(kuò)展的效果是直接包含在渲染框架內(nèi)的嗎?在bgfx上怎么實(shí)現(xiàn)的?
  • 框架的一些特點(diǎn)

這些問題首先可以幫助我了解一個(gè)優(yōu)秀的開源引擎的使用方式是什么樣子的,是否有通過什么樣的巧妙設(shè)計(jì)來讓使用方用起來更加得心應(yīng)手;接下來可以讓我學(xué)習(xí)到這個(gè)項(xiàng)目是如何做好各平臺適配的,以及時(shí)通過什么樣的方式來切換各種渲染驅(qū)動的;之后便是 bgfx 是如何設(shè)計(jì)它的渲染流程的,后續(xù)自己設(shè)計(jì)方案和實(shí)現(xiàn)時(shí)可以借鑒哪些內(nèi)容;最后就是一些擴(kuò)展性的需求是如何與核心渲染api進(jìn)行協(xié)作的,是直接包含在模塊內(nèi)部還是以組件的方式來不斷迭代。

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

使用流程

在渲染流程上我們使用 bgfx從入門到?jīng)]有放棄 里使用 FBO 渲染紋理,并顯示到屏幕上例子,這篇文章中主要是講的這個(gè)例子的使用流程,我會在這個(gè)例子里面加上一些在后續(xù)引擎開發(fā)中需要關(guān)注的點(diǎn)的分析,比如PlatformData的作用和流程、Init包含哪些數(shù)據(jù)等等。

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

  1. 初始化渲染平臺信息
  2. 初始化 bgfx 資源
  3. 設(shè)置頂點(diǎn)坐標(biāo),紋理坐標(biāo)
  4. 設(shè)置清屏色
  5. 加載紋理,shader,組裝成 program
  6. 創(chuàng)建 FBO,綁定紋理
  7. 渲染 FBO
  8. 渲染 FBO 結(jié)果紋理到屏幕
  9. 銷毀資源
  10. 銷毀 bgfx

初始化渲染平臺信息

僅以O(shè)penGL為例,有做過OpenGL開發(fā)的同學(xué)肯定知道,OpenGL的渲染跟其上下文環(huán)境息息相關(guān),在某個(gè)沒有上下文環(huán)境的線程中執(zhí)行渲染操作會導(dǎo)致沒有效果、黑屏等等問題,因此我們可以通過持有上層的GL視圖等數(shù)據(jù)資源,從而在必要時(shí)刻保證上下文環(huán)境的正確性,從而避免出現(xiàn)渲染問題。

那么假設(shè) bgfx 默認(rèn)是使用的 OpenGL ES 來實(shí)現(xiàn)渲染的話,那么上層的 view 是如何與底層的 Egl 綁定在一起的?要回答這個(gè)問題,我們得知道,OpenGL最終的渲染,都是渲染在一個(gè) EGLSurface 中,這個(gè) EGLSurface 的創(chuàng)建方式如下:

EGLSurface EGLAPIENTRY eglCreateWindowSurface (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);

其中第三個(gè)參數(shù) EGLNativeWindowType 就是和上層 view 掛鉤的,

對于 Android 平臺來說,不管上層用 NativeActity,還是 GlSurfaceView 還是 SurfaceView,都需要一個(gè)代表屏幕渲染緩沖區(qū)的類 surface 來創(chuàng)建一個(gè) 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); // 設(shè)置平臺信息,綁定上層 view

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

PlatformData

首先看一下 PlatformData 的數(shù)據(jù)結(jié)構(gòu):

struct PlatformData
{
    PlatformData();
    // 展示的類型
    void* ndt;          //!< Native display type. 
    // 用于展示最終結(jié)果的窗口,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* 類型以便接受各個(gè)平臺各類型的數(shù)據(jù)對象,而PlatformData通過 bgfx::setPlatformData 接口來進(jìn)行設(shè)置,在 bgfx.cpp 中有一個(gè)全局變量 g_platformData 持有平臺數(shù)據(jù)對象。

PlatformDataGL上下文等渲染過程中所需要的數(shù)據(jù),在bgfx中各自平臺寫了各自平臺的渲染器,如:renderer_vk.cpp,renderer._mtl.mm等,在各自的渲染器中通過全局變量 g_platformData 的數(shù)據(jù)進(jìn)行類型強(qiáng)制的方式轉(zhuǎn)換成各自平臺需要的數(shù)據(jù),如下:

m_device = (id<MTLDevice>)g_platformData.context;

同樣的各自平臺也有各自平臺的上下文文件,如:glcontext_eagl.mm,glcontext_egl.cpp等,獲取數(shù)據(jù)的方式同渲染器:

CAEAGLLayer* layer = (CAEAGLLayer*)g_platformData.nwh;

在bgfx的Demo中初始化PlatformData時(shí),發(fā)現(xiàn)除了nwh之外,其余參數(shù)均賦值為NULL

bgfx中在各自平臺的上下文文件中(如glcontext_eagl.h,glcontext_egl.h)定義了一個(gè)GlContext結(jié)構(gòu)體,結(jié)構(gòu)體中有一個(gè)m_context的成員變量也是用來存儲當(dāng)前平臺的GL上下文的。

在調(diào)用 GlContext:Create 接口時(shí)會去取全局變量 g_platformData 中的 context ,如果是空的(一般情況下為空),則創(chuàng)建各自平臺的上下文環(huán)境,并賦值給 m_context 變量,此外,還將m_context賦值給g_internalData.context ,這個(gè) g_internalData 也是bgfx_p.h聲明的一個(gè)全局類變量,類型為 InternalData。

在GlContext結(jié)構(gòu)體定義了一個(gè)isValid函數(shù)來判斷上下文是否有效,內(nèi)部實(shí)現(xiàn)是通過判斷m_context變量是否為空的方式來確定上下文環(huán)境是否有效。

初始化bgfx資源

bgfx初始化資源是用 bgfx::Init 存儲初始化信息,通過 bgfx::init(init) 接口進(jìn)行初始化。

bgfx::Init init;
// 選擇一個(gè)渲染后端,當(dāng)設(shè)置為 RendererType::Enum::Count 的時(shí)候,系統(tǒng)將默認(rèn)選擇一個(gè)平臺,可以設(shè)置Metal,OpenGL ES,Direct 等
init.type = bgfx::RendererType::Enum::Count;
// 設(shè)置供應(yīng)商接口Vendor PCI ID,默認(rèn)設(shè)置為0將選擇第一個(gè)設(shè)備來顯示。
// #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;
// 設(shè)置分辨率大小
init.resolution.width = m_width;
init.resolution.height = m_height;
// BGFX_RESET_VSYNC 其作用主要是讓顯卡的運(yùn)算和顯示器刷新率一致以穩(wěn)定輸出的畫面質(zhì)量。
init.resolution.reset = BGFX_RESET_VSYNC;
bgfx::init(init);

Init

bgfx 使用 Init 對象來存儲初始化信息,如屏幕分辨率、刷新機(jī)制、渲染框架等:

struct Init
{
    Init();
    /// 設(shè)置渲染后端,當(dāng)設(shè)置成 RendererType::Count 時(shí)會選擇一個(gè)當(dāng)前平臺的默認(rèn)渲染后端
    /// 具體可見:`bgfx::RendererType`
    RendererType::Enum type;

    /// 設(shè)置供應(yīng)商接口Vendor PCI ID,設(shè)置為 BGFX_PCI_ID_NONE 將選擇第一個(gè)設(shè)備來顯示。
    ///   - `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.
    /// 暫未研究該參數(shù)具體用處
    uint16_t deviceId;

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

    /// Platform data.
    PlatformData platformData;

    /// 設(shè)置離屏后緩沖的分辨率大小并充值參數(shù)
    /// 見:bgfx::Resolution
    Resolution resolution;

    struct Limits
    {
    uint16_t maxEncoders;     //!< encoder 線程的最大數(shù)量.
    uint32_t transientVbSize; //!< Maximum transient vertex buffer size.
    uint32_t transientIbSize; //!< Maximum transient index buffer size.
    };

    Limits limits;

    /// 接收事件回調(diào)的接口
    /// 見: `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.
    /// 暫未研究該參數(shù)具體用處
    bx::AllocatorI* allocator;
};

這里我們可以看到 Resolution 和離屏緩沖區(qū)有聯(lián)系,簡單看了一下代碼,推測是所有渲染都先渲染至離屏緩沖區(qū)中,在調(diào)用 bgfx::frame() 時(shí)再將前后屏緩沖區(qū)切換顯示渲染結(jié)果,不知道是否是為了可以用來做使用小的離屏尺寸,最后放大到視圖大尺寸之類的優(yōu)化,需要進(jìn)一步研究。

構(gòu)建頂點(diǎn)坐標(biāo)、紋理坐標(biāo)

// 封裝頂點(diǎn)對象
struct PosColorVertex {
    // 頂點(diǎn)坐標(biāo)
    float m_x;
    float m_y;
    float m_z;
    // 紋理坐標(biāo)
    int16_t m_u;
    int16_t m_v;
    // 頂點(diǎn)描述對象
    static bgfx::VertexDecl ms_decl;

    static void init() {
        // 這句話的意思是位置數(shù)據(jù)里面,前三個(gè) Float 類型是作為頂點(diǎn)坐標(biāo),后兩個(gè) Int16 類的值作為紋理的坐標(biāo)
        ms_decl
          .begin()
          .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
          .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Int16, true)
          .end();
    };
};

// 這個(gè)地方要注意了,此時(shí) FBO 的紋理坐標(biāo) Android 和 iOS 都是采用左下角作為紋理坐標(biāo)原點(diǎn),
// iOS 或者 Mac 平臺在渲染的時(shí)候,也是使用同樣的坐標(biāo)來渲染,但是 Android 平臺不一樣,
// Android 平臺在渲染紋理的時(shí)候,是采用左上角作為紋理坐標(biāo)來渲染的,
// 所以對于 Android 平臺來說,下面還需要一個(gè)渲染的坐標(biāo) 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 平臺渲染的坐標(biāo)和紋理頂點(diǎn),左上角為紋理原點(diǎn)
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},
        };

// 頂點(diǎn)繪制順序
static const uint16_t s_TriList[] =
        {
                0, 2, 1,
                1, 2, 3,
        };

設(shè)置清屏色

// 設(shè)置清屏色,0或者1或者其他數(shù)據(jù)代表 view_id 的編號,這個(gè)view內(nèi)部是個(gè)結(jié)構(gòu)體,它封裝了一個(gè)渲染的范圍,清屏色,F(xiàn)BO 等等參數(shù),用作最后渲染框架渲染的時(shí)候用
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
);

設(shè)置清屏色時(shí)會需要設(shè)置一個(gè) view_id,這個(gè)view內(nèi)部是個(gè)結(jié)構(gòu)體,它封裝了渲染的范圍,清屏色,F(xiàn)BO 等等參數(shù),用作最后渲染框架渲染的時(shí)候用,可以設(shè)置不同view的清屏色。

而這些View是用一個(gè)大小固定的數(shù)組來作為容器承載,因此view是有上限的,目前上限是256,同時(shí)這些配置的信息都存儲在一個(gè)Context的結(jié)構(gòu)體里面。

加載紋理、Shader、Program

// FBO 頂點(diǎn)緩沖區(qū) Handle
bgfx::VertexBufferHandle m_vbh;
// Android 渲染頂點(diǎn)緩沖區(qū) Handle
bgfx::VertexBufferHandle m_vbh_Android_render;
// 頂點(diǎn)繪制順序緩沖 Handle
bgfx::IndexBufferHandle m_ibh;

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

// 用于顯示的 program
bgfx::ProgramHandle m_display_program;
// 用于顯示的紋理,此時(shí)來自于 FBO 的結(jié)果
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 創(chuàng)建 program
m_program = loadProgram("vs_cubes", "fs_cubes");
// shader的uniform
s_textureHandle = bgfx::createUniform("s_texColor", bgfx::UniformType::Int1);
// 創(chuàng)建紋理
m_texture = loadTexture("/sdcard/test04.jpg");

// 創(chuàng)建顯示的 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 中每個(gè)Buffer、紋理、Program等都會有一個(gè)對應(yīng)的Handle對象,并且通過bgfx的 creatXXX 接口來創(chuàng)建,這些接口基本都需要又一個(gè)makeRef態(tài)方法創(chuàng)建的對象。

makeRef 會創(chuàng)建一個(gè) Memory 數(shù)據(jù),里面主要存儲著又是 void* 類型的各種數(shù)據(jù),以及數(shù)據(jù)大小等,方便后續(xù)的 createXXX 讀取數(shù)據(jù)創(chuàng)建對應(yīng)Handle等對象。

bgfx 通過定義以下宏來快速實(shí)現(xiàn)各種數(shù)據(jù)的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)

創(chuàng)建FBO,綁定紋理

// 切記 bgfx 的 FBO 初始化要定義成BGFX_INVALID_HANDLE,不然要被坑
bgfx::FrameBufferHandle m_fbh = BGFX_INVALID_HANDLE,;
// 不設(shè)置成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

// 設(shè)置渲染窗口大小
bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height));
// 綁定 FBO 到 View_Id 為0的這個(gè) View 上,開始渲染,渲染開始是 submit 方法調(diào)用后。
bgfx::setViewFrameBuffer(0, m_fbh);
bgfx::setState(BGFX_STATE_WRITE_RGB|BGFX_STATE_WRITE_A);
// 設(shè)置 FBO 需要的輸入紋理
bgfx::setTexture(0, s_textureHandle, m_texture);
bgfx::submit(0, m_program);

該步驟同樣通過指定 view_id 進(jìn)而將參數(shù)綁定到對應(yīng)的視圖上,并且最后通過 bgfx::submit(view_id, program_handle) 來提交數(shù)據(jù);

submit 接口通過Context內(nèi)部的Encoder調(diào)用subitmit接口,Encoder是用來負(fù)責(zé)提交來自多個(gè)線程的渲染指令的,一個(gè)線程只會有一個(gè)Encoder,通過bgfx::begin來獲取,Encoder內(nèi)部同時(shí)存儲了變換矩陣、坐標(biāo)buffer等等信息的原始數(shù)據(jù)。

setViewFrameBuffer 將 FrameBufferHandle 賦值給了對應(yīng) View 對象的 m_fbh 成員變量,而 setState 最終是到 EncoderImpl 的 setState 接口中,光看代碼感覺像是設(shè)置混合模式之類的,而且還會影響到透明度排序,這里不太清楚具體的用處。

渲染FBO結(jié)果紋理到屏幕

// 渲染到屏幕的 view 需要主動將該 view 的 FBO 設(shè)置為 invalid,然后從 FBO 中拿出 attach 的紋理,設(shè)置到這次渲染需要的輸入?yún)?shù)中,然后顯示
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();

該步驟額外設(shè)置了頂點(diǎn)坐標(biāo),接口第一個(gè)參數(shù)為0,后續(xù)操作多了一個(gè)bgfx::frame()用于將結(jié)果顯示到屏幕的操作

銷毀資源

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刪除傳入的數(shù)據(jù)handle。

銷毀接口用多態(tài)的方式來銷毀多種Handle,內(nèi)部最終還是寫入到 CommandBuffer 中。

銷毀bgfx

bgfx::shutdown();

渲染流程

上面主要介紹了 bgfx的一個(gè)簡單的使用流程,并在這個(gè)使用流程中穿插了一些我們自己研究項(xiàng)目源碼的收獲,接下來開始梳理一下bgfx的渲染流水線是什么樣的,這里以 bgfx 的 Cube 例子作為研究的Demo,以O(shè)penGL作為渲染后端。

接下來是關(guān)于梳理渲染流程時(shí)的一些梳理路程以及一些點(diǎn)的總結(jié),不感興趣的話可以直接跳過看流程圖。

一開始在大致瀏覽了一下 bgfx 的目錄/文件結(jié)構(gòu)時(shí),發(fā)現(xiàn)了一個(gè) renderer_gl.cpp 文件,里面定義了一個(gè) biltRender 的接口(該接口內(nèi)部調(diào)用了 glDrawElements 方法),通過斷點(diǎn)該接口發(fā)現(xiàn)渲染調(diào)用時(shí)在(MAC OS)entry_osx.mmrun 方法中,該方法會一直循環(huán)直到程序退出。

entry_osx.mm 在462行調(diào)用了 bgfx::renderFrame() 方法,逐級向下依次調(diào)用以下接口:

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行)

但是這時(shí)候發(fā)現(xiàn)內(nèi)部的glDrawElements沒有通過判斷,因此實(shí)際上并沒有被調(diào)用到。

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

GL_CHECK(glDrawElementsInstanced(prim.m_type
                                        , numIndices
                                        , indexFormat
                                        , (void*)(uintptr_t)(draw.m_startIndex*indexSize)
                                        , draw.m_numInstances
                                        ) );

在上述接口2次調(diào)用后,第一次斷點(diǎn)排查的接口:

_renderCtx->blitRender(_blitter, numIndices);(renderer_gl.cpp 803行)

內(nèi)部的 glDrawElements 也會被調(diào)用,此后便是保持 2次+1次 的方式循環(huán),這些和bgfx的設(shè)計(jì)有關(guān)系,暫時(shí)不去關(guān)心,目前只重點(diǎn)關(guān)注整體的渲染流程,類似于在一些場景下也會調(diào)用 glDrawArrays 而不是 glDrawElements ,但是這個(gè)例子里面沒有。

執(zhí)行完渲染后回到 bgfx.cpp 的 renderFrame 的 2300 行執(zhí)行 rendererExecCommands 提交渲染后指令,一次渲染的流程差不多到這里就結(jié)束了。

流程圖

image-20190315202714262

CommandBuffer

bgfx 的頂點(diǎn)數(shù)據(jù)等信息的設(shè)置,都是先緩存在類似 RenderDraw 之類的對象中,RenderDraw 這些對象又緩存在對應(yīng)的 Encoder 中,Encoder 又依附在 Context 上,最后渲染的時(shí)候?qū)⑦@些信息一個(gè)個(gè)commit批量使用gl命令來進(jìn)行實(shí)際的執(zhí)行操作,這樣可以在這里做batch等優(yōu)化操作。

這些渲染命令分為在渲染前執(zhí)行和渲染后執(zhí)行兩種,統(tǒng)一由 CommandBuffer 來管理,bgfx 是在用戶調(diào)用createXXXBuffer (如創(chuàng)建頂點(diǎn)數(shù)據(jù)Buffer)之類接口調(diào)用時(shí),間接調(diào)用 Context 的 createXXXBuffer,其內(nèi)部以 Buffer 的類型(在 CommandBuffer 中用枚舉定義了各種命令類型)來判斷是用前置命令還是后置命令(通過 getCommandBuffer 來獲取),然后使用不同的 CommandBuffer(前置為 m_submit->m_cmdPre,后置為 m_submit->m_cmdPost )來寫入數(shù)據(jù),接下來在renderFrame的時(shí)候再從這兩個(gè)Buffer里讀取此前寫入的數(shù)據(jù),并調(diào)用對應(yīng)驅(qū)動如 renderer_gl.cpp 下的 RendererContextI 來設(shè)置數(shù)據(jù)。

Encoder

另外在 bgfx 用例里面的 update() 刷新接口中設(shè)置的渲染數(shù)據(jù)可以分為兩類,一類是和View相關(guān)的如 setViewRect 之類的,這類會直接通過 Context 的成員函數(shù)進(jìn)行設(shè)置,另一類是setUniform等和View無關(guān)的,這類會通過 Context 的 Encoder 間接調(diào)用對應(yīng)的接口,而這些接口調(diào)用又通過一個(gè)定義的宏 BGFX_ENCODER 轉(zhuǎn)接到 EncoderImpl 上去(在項(xiàng)目中經(jīng)??吹竭@樣類似的宏),而 EncoderImpl 中大多就是將這些參數(shù)寫入一個(gè)個(gè)Buffer或者其內(nèi)部數(shù)據(jù)成員中,最終在 renderer_gl.cpp 之類對應(yīng)驅(qū)動的渲染器內(nèi)部的 submit 等接口中,通過將數(shù)據(jù)存儲在 Frame 里的 RenderItem 再里面的 RenderDraw 等一系列對象中,并在該接口內(nèi)部完成綁定。

而 EncoderImpl 保存數(shù)據(jù)的 Frame 和 bgfx 傳遞給具體 Renderer(即 m_render ) 的 submit 的 Frame 是怎么同步的呢,這里其實(shí)在 Context::begin 的時(shí)候會調(diào)用 EncoderImpl::begin ,同時(shí)傳入其 m_submit 成員,而該成員在非多線程的情況下與 m_render 是同一個(gè)對象,而至于在多線程情況下目前還沒有研究到。

CommandBuffer / Encoder

以目前閱讀代碼下來的收獲來看,CommandBuffer 主要用于創(chuàng)建 VertexBuffer、Shader等GL資源或者執(zhí)行GL指令,而 Encoder 則用來記錄各種參數(shù)如變換矩陣等等,在渲染前通過 Frame 帶上這些數(shù)據(jù)綁定到著色器里對應(yīng)的變量上。

切換渲染驅(qū)動

這部分網(wǎng)上已經(jīng)有同學(xué)研究過了,具體過程可見 bgfx入門練習(xí)1——切換圖形API驅(qū)動模式DX與OpenGL 以及 bgfx入門練習(xí)2——找出DX,OpenGL驅(qū)動切換實(shí)現(xiàn)原理 ,這里列出一些摘要。

bgfx 首先在 demo 調(diào)用 init 的時(shí)候會去判斷使用什么引擎,在 config.h 頭部寫了這些驅(qū)動的判定,如果什么都沒定義,就怎么怎么樣之類的,這些驅(qū)動在一個(gè)叫 s_rendererCreator 的數(shù)組中,搜索這個(gè)數(shù)組,來到bgfx.cpp的 rendererCreate()函數(shù)中,在 if(s_rendererCreator[ii].supported) 這句下斷點(diǎn),跟了下,就知道寫了個(gè)評估算法,score最高的是DX11。

bgfx_utils.cpp 中有一個(gè) loadShader 的靜態(tài)方法,該函數(shù)內(nèi)部有不同驅(qū)動的分支判斷,但是繼續(xù)跟又?jǐn)嗔恕?/p>

查看 bgfx 的 src 目錄,發(fā)現(xiàn)在 renderer 下面有 shader.cpp、shader_dxbc.cpp 等,如在DX驅(qū)動模式下, shader_dxbc.cpp 的 555 行看到 readString 方法,下斷點(diǎn),成功斷下來,換到Opengl下無效,可以判斷此處應(yīng)為DX轉(zhuǎn)換代碼,順便BC應(yīng)該是Byte Code的意思。

之后在堆棧里向上找,找到一處多態(tài)調(diào)用 bgfx.cpp,2405 行 case CommandBuffer::CreateShader: 這個(gè)分支下面有一句 m_renderCtx->createShader(handle, mem); 就是這里做了多態(tài)處理。

繼續(xù)跟可以發(fā)現(xiàn),如果是 OGL,就直接在 renderer_gl.cpp 中做 m_id = glCreateShader(m_type); 從而建立GL Shader。如果是DX,從字節(jié)碼判定,然后在 renderer_d3d11.cpp 中 CreateVertexShader CreatePixelShader CreateComputeShader 。包括后續(xù)的渲染等操作也是通過這樣的多態(tài)方式進(jìn)行。

看到這里就有一個(gè)感觸,策略模式真是一招鮮,吃遍天啊。

文字繪制

通過 freetype 等第三方庫提供支持,可支持加載 ttf 字體文件。

font_manager.h 中看到了一個(gè) GlyphInfo 的結(jié)構(gòu)體,內(nèi)部有關(guān)于 x、y 偏移等控制的成員變量,但是看接口定義只有加載文字生成對應(yīng)GlyphInfo數(shù)據(jù)(在 Font Demo 中是通過 font_manager.cpp 的 523 行的 FontManager::preloadGlyph 接口),而沒有看到通過 GlyphInfo 去控制繪制的情況,我們通過手動修改 preloadGlyph 里面的最終結(jié)果值,如 glyphInfo.advance_x ,可控制字與字之間x方向間距,因此可推斷支持簡單的文字排版操作,具體如何實(shí)現(xiàn)、在細(xì)節(jié)處如何流轉(zhuǎn)后續(xù)繼續(xù)跟蹤。

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

多邊形繪制

類似文字繪制,是通過 nanovg 輔助實(shí)現(xiàn)的,具體在 bgfx 中如何交互實(shí)現(xiàn)的也需要屆時(shí)進(jìn)一步研究。

效果系統(tǒng)擴(kuò)展

在Demo中已有看到延遲渲染、粒子系統(tǒng)、光照等效果或?qū)崿F(xiàn)方案,因此可見 bgfx 也已支持這些常見功能,具體實(shí)現(xiàn)方案如粒子效果Demo是在 update() 接口中接入自己的 psUpdate/psRender 等渲染流程,因此目前來看這些額外的功能都是bgfx基于核心渲染API之上額外疊加的系統(tǒng),類似于一個(gè)個(gè)的插件掛靠在bgfx的核心渲染系統(tǒng)上,而bgfx的核心庫只提供基礎(chǔ)渲染api的封裝,而具體每一個(gè)效果在其上如何實(shí)現(xiàn)后續(xù)再進(jìn)行研究。

其他

不管是從創(chuàng)建還是到后面的設(shè)置等流程中,bgfx 都會以一系列的 Handle 來傳遞這些數(shù)據(jù),再簡單看了下 bgfx 的 src 目錄,里面 glcontext_xxx 文件分為了 egl、eagl、nsgl 以及 wgl 等平臺,用以通過各個(gè)平臺自己的方式來管理 Surface、Context 等對象。而上面提到過的 renderer_xxx 則用來實(shí)現(xiàn)各個(gè)不同驅(qū)動下的渲染邏輯,同時(shí)還有一些 shader_xxx 文件,推測是用來適配特定驅(qū)動/驅(qū)動版本的著色器相關(guān)功能的。

bgfx 需要依賴于另外兩個(gè)庫才能運(yùn)行,一個(gè)是 bimg 用來做圖像的編解碼等工作,還有一個(gè) bx 的基礎(chǔ)庫,用以提供線程、調(diào)試等一系列的基礎(chǔ)工具,這些庫的核心代碼基本都直接一股腦地放在倉庫的src目錄下,感興趣的可以去翻一翻。

總結(jié)

在本章中我們首先列舉了希望從 bgfx 這個(gè)項(xiàng)目中學(xué)到哪些內(nèi)容;緊接著介紹了一個(gè)簡單的使用流程,并在各個(gè)節(jié)點(diǎn)中插入了對一些細(xì)節(jié)的分析和整理;然后我們依次完成了對渲染流程、切換渲染驅(qū)動、文字與多邊形繪制、效果系統(tǒng)擴(kuò)展等方面或較詳細(xì)或簡單的分析。

通過上述的這些分析,我們在開篇中提到的幾個(gè)問題也基本都得到了解決,但是只有一個(gè)項(xiàng)目心里難免還是有些懷疑,是不是所有的渲染引擎都是這樣差不多的流程,有沒有更好的實(shí)現(xiàn)方式等等,因此在真正開始 coding 之前,還會在 ogreUrho3D 中繼續(xù)選擇一個(gè)進(jìn)行分析,最后匯總這三篇文章的內(nèi)容,并定下渲染引擎后續(xù)的框架、渲染流程等等內(nèi)容,然后碼下見功夫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom閱讀 3,205評論 0 3
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,666評論 1 32
  • 本篇文章是基于谷歌有關(guān)Graphic的一篇概覽文章的翻譯:http://source.android.com/de...
    lee_3do閱讀 7,472評論 2 21
  • Android平臺下OpenGL初步 Android OpenGL ES 開發(fā)教程 從入門到精通http://bl...
    garyhu1閱讀 1,643評論 1 2
  • 我喜歡水 靜靜地看著它 隨著水浪起伏 時(shí)而碧波漣漪 時(shí)而平淡無奇 心也隨著它平靜
    黎小小小閱讀 261評論 0 0

友情鏈接更多精彩內(nèi)容