跨平台渲染引擎之路:bgfx分析
前言
前文我们完成一些在开始跨平台渲染引擎之路前所需要的铺垫工作中的一部分:基础信息收集,并且在最后梳理出了一些开源引擎来作为我们接下来的研究对象,从这些大牛的成果中我们可以学习到很多成熟的实现方案和设计思路,这些一方面能帮助我们快速成长,另一方面可以帮助我们在真正开始实现引擎前制定一个符合我们需求并且大方向上不出错的设计方案。
工欲善其事,必先磨其器,一个完善而正确的设计方案可以在后面落地实现的过程中不断地指导我们的开发方向,同时也避免了后续频繁地大规模重构甚至重写的恶心事情发生,因此这个前期预研并确定方案的步骤是至关重要而且必须的。
本篇文章我们先分析 bgfx 这个项目,至少 bgfx 可以用来做什么、怎么编译之类的就不多做介绍了,官方文档都有。
Tips:该文章基于 bgfx 的 bd2bbc84ed90512e0534135b1fcd51d02ae75c69(SHA1值)提交进行分析
从问题出发
如果每个引擎的研究我们都逐行代码地看下去,那么要耗费较长的时间不说,而且收获到也不一定都是我们真正需要的,整体的效率就会显得非常低下,每个开源项目都有很多我们可以学习也有很多是个人开发/设计习惯所致的结果,因此在这里我们一样和上一篇中一样,带着问题出发,先思考我们想要从这个引擎中学到哪些东西。
以我个人的角度出发,我希望搞清楚的以下几个内容:
- 简单的使用流程是怎么样的
- 主要的渲染流水线是怎么样的?有哪些比较核心的类?
- 是如何实现切换渲染驱动的
- 是否需要持有平台数据?又是如何持有和使用的
- 文字、多边形的绘制是怎么实现的?
- 粒子、光照这些扩展的效果是直接包含在渲染框架内的吗?在bgfx上怎么实现的?
- 框架的一些特点
这些问题首先可以帮助我了解一个优秀的开源引擎的使用方式是什么样子的,是否有通过什么样的巧妙设计来让使用方用起来更加得心应手;接下来可以让我学习到这个项目是如何做好各平台适配的,以及时通过什么样的方式来切换各种渲染驱动的;之后便是 bgfx 是如何设计它的渲染流程的,后续自己设计方案和实现时可以借鉴哪些内容;最后就是一些扩展性的需求是如何与核心渲染api进行协作的,是直接包含在模块内部还是以组件的方式来不断迭代。
那么就按照上面的问题顺序,启程!
使用流程
在渲染流程上我们使用 bgfx从入门到没有放弃 里使用 FBO 渲染纹理,并显示到屏幕上例子,这篇文章中主要是讲的这个例子的使用流程,我会在这个例子里面加上一些在后续引擎开发中需要关注的点的分析,比如PlatformData的作用和流程、Init包含哪些数据等等。
在该例子中我们可以大概列出以下的步骤:
- 初始化渲染平台信息
- 初始化 bgfx 资源
- 设置顶点坐标,纹理坐标
- 设置清屏色
- 加载纹理,shader,组装成 program
- 创建 FBO,绑定纹理
- 渲染 FBO
- 渲染 FBO 结果纹理到屏幕
- 销毁资源
- 销毁 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