5评论

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

格子 2019-03-16 1.5k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏美术行业精英群167422913

前言

前文我们完成一些在开始跨平台渲染引擎之路前所需要的铺垫工作中的一部分:基础信息收集,并且在最后梳理出了一些开源引擎来作为我们接下来的研究对象,从这些大牛的成果中我们可以学习到很多成熟的实现方案和设计思路,这些一方面能帮助我们快速成长,另一方面可以帮助我们在真正开始实现引擎前制定一个符合我们需求并且大方向上不出错的设计方案。

工欲善其事,必先磨其器,一个完善而正确的设计方案可以在后面落地实现的过程中不断地指导我们的开发方向,同时也避免了后续频繁地大规模重构甚至重写的恶心事情发生,因此这个前期预研并确定方案的步骤是至关重要而且必须的。

本篇文章我们先分析 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 提交渲染后指令,一次渲染的流程差不多到这里就结束了。

流程图

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 中继续选择一个进行分析,最后汇总这三篇文章的内容,并定下渲染引擎后续的框架、渲染流程等等内容,然后码下见功夫。

腾讯游戏学院公众号