前言

因为最近事情太多,导致原本每月至少一篇的计划整整延迟了一个季度,所幸现在大多事情都比较明确和稳定了,因此接下来会保持基本每月至少一篇的进度,同时渲染库也会正式开始进行,感兴趣的提前Star关注一下~

GPUImage-X :以游戏引擎的效果和特性,支持图片/视频等场景下的跨平台3D渲染库

不一样的渲染库

在之前研究阶段我们看到的大多都是以游戏为需求出发的游戏引擎,这类引擎一方面代码量庞大,架构复杂,包罗万象,在现阶段直接以游戏引擎或者类似bgfx的渲染引擎为目标从0开始撸一套明显会有些力不从心,而且这个重复造轮子的程度就有点高了,因此是否可以有一种方式能让我们既可以从中学习到渲染引擎以及之上的各个效果系统的实现方式,同时又能去适应其他不同的场景,增加我们的成就感呢?

现在市场上各类社交、工具中都有视频和图片的场景,并且用户都喜欢在上面添加各类特效,甚至也已经看到了各种3D的玩法,但是在我尝试使用Urho3D等游戏引擎来适配这类场景的时候,发现使用难度较大,因为这些游戏引擎的初衷并不是为了这样的场景而设计的,引擎内的很多系统也都是这类场景所不需要的,导致编译出来的库也较大,同时学习成本也是难以想象的。

而目前开源的库中添加图像效果的最主流的就是GPUImage这个库,但是这个库基本只能添加图像处理方面的滤镜效果,有Transform的滤镜但是并不能符合3D的需求,同时粒子效果、光照等目前也是不支持的。

因此在接下来这段时间里,我会以bgfx来提供底层渲染API的封装和多平台渲染后端的支持,在之上搭建支持这类场景(图片/视频叠加特效的同时还需要进行一些3D渲染、粒子等更丰富的内容)的跨平台渲染库,架构设计等内容正在准备中,因为这个库理论上是在GPUImage的场景支持基础上进行扩展,让能力变得更加强大,同时将其变为跨平台的一个库,因此命名为 GPUImage-X ,预计本月内会完成整体框架的搭建并先可渲染滤镜链,届时也会针对架构设计、流程设计、规划等方面发表一篇文章,感兴趣的可以先Star一下~

bgfx实战

既然我们确定在bgfx之上搭建我们的渲染库,那么就需要先在bgfx上小试牛刀一下,至少对bgfx我们第一阶段比较关心的一些用法比较清楚,而第一阶段的目标是完成整体框架的落地以及实现其中的滤镜链系统,因此我们先以以下几个点作为切入,在bgfx之上写一个简单的Demo来确定使用方式:

  • 渲染一张纹理
  • 在纹理上叠加滤镜链
  • 多纹理混合

创建Demo

为了避免修改bgfx自身所带的Demo,所以我们自己创建一个调试用的Demo。

首先在bgfx/examples目录下创建一个名为41-filter的目录,在该目录内放置以下文件:

  • filter.cpp:Demo代码
  • fs_filter_bw.sc:bgfx着色器规则下的将纹理渲染为灰度图的片段着色器
  • fs_filter_normal.sc:bgfx着色器规则下的将纹理简单渲染出来的片段着色器
  • makefile:配置编译规则,可在编译的时候直接将我们的着色器编译成bgfx支持的.bin格式文件放置到runtime/shaders的各个着色语言目录下
  • varying.def.sc:着色器的参数声明
  • vs_filter_normal.sc:bgfx着色器规则下顶点着色器

具体代码内容见fork的bgfx,后续有些效果为了快速验证也会先在这个仓库上先进行Demo验证,然后实现到GPUImage-X上,而语法方面大家如果熟悉glsl,通过官方说明和配合其他Demo里的着色器摸索一下应该问题不大。

在添加上述文件后,还需要修改以下两个文件:

  • examples/makefile:在@make -s --no-print-directory rebuild -C 40-svt下面加一行:@make -s --no-print-directory rebuild -C 41-filter
  • scripts/genia.lua:在, "40-svt"下面加一行:, "41-filter"

上面操作做完之后,命令行下回到bgfx的根目录,按照官方的指令编译出工程(笔者是在Mac OS的环境下):

  • ../bx/tools/bin/darwin/genie —with-combined-examples —xcode=osx xcode9
  • open .build/projects/xcode9-osx/bgfx.xcworkspace

运行examples-debug,即可在Demo列表的最下面看到我们的41-filterDemo了。

笔者自己手动在runtimes/iamges下放了一张fengjing.jpg的图片作为纹理显示,大家可以随便放一个图片进去,命名一致即可,如果不一致就需要修改filter.cpp里的记载的纹理名

shaderc

首先通过上面的操作我们会发现,在编译结束后脚本并不会自动将这些着色器编译成各类着色器语言的.bin文件并放置到runtime/shaders下(暂未找到在哪里配置,后续嫌麻烦找到了再补上~),而runtime这个目录里正式存储着所有在运行时会用到的资源(包括上面的图片),因此除了在一开始我们就配置好编译出来,在过程中我们也可以直接往runtime下面扔资源,Demo也是可以找得到的。

那么这时候我们就可以用到bgfx提供给我们的shaderc工具了。

首先在clone下来的bgfx仓库中是没有shaderc的可执行文件的,因此需要先进行编译,在bgfx的根目录下运行make tools,然后等你打把游戏回来之后,就会发现tools/bin/darwin下面有了一些执行文件。

结合下我们上面Demo里的着色器相关的几个文件,可以发现在bgfx里面着色器的编译一般包含以下三个文件:

  • 顶点着色器
  • 片段着色器
  • 着色器参数说明

而编译的时候顶点着色器和片段着色器是分开编译,但是必须都声明其着色器说明文件路径,以41-filter为例,首先我们先命令行cd到该目录下,然后以下两条命令即是表示编译Mac OS下的Metal着色器:

  • ../../tools/bin/darwin/shaderc -f vs_filter_normal.sc -o vs_filter_normal.bin —depends -i ../../src —varyingdef varying.def.sc —platform osx -p metal —type vertex
  • ../../tools/bin/darwin/shaderc -f fs_filter_bw.sc -o fs_filter_bw.bin —depends -i ../../src —varyingdef varying.def.sc —platform osx -p metal —type fragment

第一条是顶点着色器,第二条是片段着色器,生成后把.bin文件放置到runtime/shaders/metal下即可,具体参数的说明,直接命令行调用shaderc也会出现帮助信息

渲染一张纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/*
* Copyright 2011-2019 Branimir Karadzic. All rights reserved.
* License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause
*/

#include <bx/uint32_t.h>
#include "common.h"
#include "bgfx_utils.h"
#include "imgui/imgui.h"

namespace
{

//顶点以及纹理坐标对象
struct PosTexVertex
{
float m_x;
float m_y;
float m_z;
float t_x;
float t_y;

static void init()
{
ms_decl
.begin()
.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float)
.add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float)
.end();
};

static bgfx::VertexDecl ms_decl;
};

bgfx::VertexDecl PosTexVertex::ms_decl;
//顶点坐标数据
static PosTexVertex s_vertices[] =
{
{-1.0f, 1.0f, 0.0f, 0.0f, 0.0f },
{ 1.0f, 1.0f, 0.0f, 1.0f, 0.0f },
{-1.0f, -1.0f, 0.0f, 0.0f, 1.0f },
{ 1.0f, -1.0f, 0.0f, 1.0f, 1.0f },
};
//顶点顺序
static const uint16_t s_triList[] =
{
0, 1, 2, // 0
1, 3, 2,
};

class ExampleFilter : public entry::AppI
{
public:
ExampleFilter(const char* _name, const char* _description)
: entry::AppI(_name, _description)
{
}

void init(int32_t _argc, const char* const* _argv, uint32_t _width, uint32_t _height) override
{
Args args(_argc, _argv);

m_width = _width;
m_height = _height;
m_debug = BGFX_DEBUG_TEXT;
m_reset = BGFX_RESET_VSYNC;

bgfx::Init init;
init.type = args.m_type;
init.vendorId = args.m_pciId;
init.resolution.width = m_width;
init.resolution.height = m_height;
init.resolution.reset = m_reset;
bgfx::init(init);

// Enable debug text.
bgfx::setDebug(m_debug);

// Set view 0 clear state.
bgfx::setViewClear(0
, BGFX_CLEAR_COLOR|BGFX_CLEAR_DEPTH
, 0x303030ff
, 1.0f
, 0
);

// Create vertex stream declaration.
PosTexVertex::init();

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

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

// Create program from shaders.
m_program = loadProgram("vs_filter_normal", "fs_filter_normal");
m_texture = loadTexture("images/fengjing.jpg");
u_Texture = bgfx::createUniform("s_texColor", bgfx::UniformType::Sampler);

imguiCreate();
}

virtual int shutdown() override
{
imguiDestroy();

bgfx::destroy(m_ibh);
bgfx::destroy(m_vbh);
bgfx::destroy(m_program);
bgfx::destroy(m_texture);
bgfx::destroy(u_Texture);

// Shutdown bgfx.
bgfx::shutdown();

return 0;
}

bool update() override
{
if (!entry::processEvents(m_width, m_height, m_debug, m_reset, &m_mouseState) )
{
imguiBeginFrame(m_mouseState.m_mx
, m_mouseState.m_my
, (m_mouseState.m_buttons[entry::MouseButton::Left ] ? IMGUI_MBUT_LEFT : 0)
| (m_mouseState.m_buttons[entry::MouseButton::Right ] ? IMGUI_MBUT_RIGHT : 0)
| (m_mouseState.m_buttons[entry::MouseButton::Middle] ? IMGUI_MBUT_MIDDLE : 0)
, m_mouseState.m_mz
, uint16_t(m_width)
, uint16_t(m_height)
);

showExampleDialog(this);

imguiEndFrame();

// Set view 0 default viewport.
bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height) );

// This dummy draw call is here to make sure that view 0 is cleared
// if no other draw calls are submitted to view 0.
bgfx::touch(0);

uint64_t state = 0
| BGFX_STATE_WRITE_R
| BGFX_STATE_WRITE_G
| BGFX_STATE_WRITE_B
| BGFX_STATE_WRITE_A
| UINT64_C(0)
;

bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height)); //设置View0的视口
bgfx::setVertexBuffer(0, m_vbh);//设置stream0的vertexBuffer,注意第一个参数不是viewId
bgfx::setIndexBuffer(m_ibh);//设置顶点索引buffer数据
bgfx::setState(state);//设置控制绘制信息的标志位
bgfx::setTexture(0, u_Texture, m_texture);//设置对应的u_Texture这个着色器参数的纹理资源
bgfx::submit(0, m_program);//提交绘制单张纹理的Program

// Advance to next frame. Rendering thread will be kicked to
// process submitted rendering primitives.
bgfx::frame();

return true;
}

return false;
}

entry::MouseState m_mouseState;

uint32_t m_width;
uint32_t m_height;
uint32_t m_debug;
uint32_t m_reset;

bgfx::VertexBufferHandle m_vbh;
bgfx::IndexBufferHandle m_ibh;
bgfx::TextureHandle m_texture;
bgfx::UniformHandle u_Texture;
bgfx::ProgramHandle m_program;
};

} // namespace

ENTRY_IMPLEMENT_MAIN(ExampleFilter, "41-filter", "Initialization and show filter effects.");

必要的地方都有加上了注释,首先我们用一个顶点数据对象来存放我们的顶点数据,定义了顶点坐标和索引,在界面初始化时加载了Program和纹理等资源,在update刷新的时候我们通过bgfx对视口、VBO等内容进行了设置,最终提交了要绘制的Program,这时候bgfx会绘制到backbuffer上,我们调用bgfx::frame()将backbuffer内容显示到屏幕上,主体流程与我们平常自己使用OpenGL还是比较接近的。

最后不要忘了在shutdown中释放资源,如果忘记释放资源,bgfx的日志会提醒存在泄漏,这点我觉得是非常不错的。

叠加滤镜链

首先我们再声明一个ProgramHandle,用于加载灰度滤镜:

1
2
bgfx::ProgramHandle m_chain_program;
m_chain_program = loadProgram("vs_filter_normal", "fs_filter_bw");

然后创建一个FrameBuffer来缓存第一个滤镜的结果:

1
2
bgfx::FrameBufferHandle m_frameBuffer;
m_frameBuffer = bgfx::createFrameBuffer(m_width, m_height, bgfx::TextureFormat::BGRA8);

最后修改我们update中绘制的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// This dummy draw call is here to make sure that view 0 is cleared
// if no other draw calls are submitted to view 0.
bgfx::touch(0);
bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height) );//设置View0的视口
bgfx::setViewFrameBuffer(0, m_frameBuffer);//设置绘制到FrameBuffer上

bgfx::touch(1);
bgfx::setViewRect(1, 0, 0, uint16_t(m_width), uint16_t(m_height) );
bgfx::setViewFrameBuffer(1, BGFX_INVALID_HANDLE);

//声明和设置我们的绘制顺序
bgfx::ViewId order[] =
{
0,1
};
bgfx::setViewOrder(0, BX_COUNTOF(order), order);

bgfx::setVertexBuffer(0, m_vbh);//设置stream0的vertexBuffer,注意第一个参数不是viewId
bgfx::setIndexBuffer(m_ibh);//设置顶点索引buffer数据
bgfx::setState(state);//设置控制绘制信息的标志位
bgfx::setTexture(0, u_Texture, m_texture);//设置对应的u_Texture这个着色器参数的纹理资源
bgfx::submit(0, m_program);//提交绘制单张纹理的Program

bgfx::setVertexBuffer(0, m_vbh);
bgfx::setIndexBuffer(m_ibh);
bgfx::setState(state);
bgfx::setTexture(0, u_Texture, bgfx::getTexture(m_frameBuffer));//设置对应的u_Texture这个着色器参数的纹理资源,即上一次的绘制结果
bgfx::submit(1, m_chain_program);

基本上和我们OpenGL绘制滤镜链也是相近的,需要注意的点是bgfx再submit之后之前的数据都会清除,所以需要重新设置,同时需要设置一下渲染的view的顺序。

多纹理混合

多纹理混合这块暂时没有写demo,因为这块比较简单,通过bgfx::setState的时候通过BGFX_STATE_BLEND_FUNC(BGFX_STATE_BLEND_ONE, BGFX_STATE_BLEND_SRC_COLOR)这样的方式来制定混合模式即可,当然也可以通过自己写着色器来进行混合。

题外话

bgfx这个库在代码实现上有些地方的确比较难看懂,而且注释、文档也比较简洁,但是整个库的设计思路还是很不错的,通过这个库可以学到很多东西,比如在renderer_gl中搜索CASE_IMPLEMENT_UNIFORM,在找找上下文,会发现bgfx通过glGetProgramiv(m_id, GL_ACTIVE_ATTRIBUTES, &activeAttribs )之类的接口来获取Program中所有的参数,然后将参数长度、类型等信息存储到自定义的数据结构中,最后在通过宏定义和组装的方式,简洁地进行了参数设置。

并且bgfx自身也提供了许多Demo,通过这些Demo基本可以学到大多数渲染效果的实现方式,这个库的作者还有一个聊天室,回复的速度基本是当天之内,所以还是非常不错和值得学习的一个库的。

总结

本篇主要介绍了一下接下来要开工的GPUImage-X的适用场景,即将3D、粒子、动画等效果融合到特效中,并主打图片/视频这类场景中,同时底层会直接使用bgfx来作为渲染API封装层,预计在本月内会完成初版的落地,因此下一篇文章将会介绍该库的框架、模块、内部流程以及将来更细致的规划,还会包括在实现过程中踩过的坑和技术点,感兴趣的可以先关注一下,这次保证不跳票~