一看就懂的OpenGL ES教程——再谈OpenGL工作机制

5,949 阅读10分钟

通过阅读本文,你将获得以下收获:
1.OpenGL的客户端-服务端模型是怎么工作的。 2.什么的OpenGL的状态机机制。

回顾上一篇

上一篇轻松入门OpenGL ES——图形渲染管线的那些事 已经主要叙述了图形渲染管线的工作机制,但是单纯知道图形渲染管线机制,恐怕还不足以让开发者真正左右OpenGL这个大块头,我们还需要站在更高的位置去看OpenGL,还需要再学习一些工作机制的内容,才可以真正写好代码。

所以本文依旧不讲代码,以防止初学者掉入细节的森林不可自拔,重在宏观工作机制,掌握工作机制之后写好代码是水到渠成的事。后面部分会开始讲解OpenGL ES的内容。

1658027123297.png

客户端-服务端模型

首先要提到的第一个,就是开发者和图形渲染管线如何交互的问题。看了上一篇文章,可能有些读者会误以为写OpenGL程序就是在写图形渲染管线,其实不是,图形感染管线整个流程的处理是在硬件(一般是gpu)内部处理的,整个流程是我们无法改变的,只是为了扩展灵活性,图形渲染管线对开发者暴露了其中若干个处理步骤交由我们灵活处理,让我们得以通过写代码的方式,也就是着色器程序,以满足我们天花乱坠的各种需求。

图形渲染管线就像手机生产的流水线,整个流水线大部分流程都是对我们封闭的,只是暴露了若干个流程得以让我们操作,比如**只暴露了手机壳的形状和颜色(着色器)**让我们自主DIY。

而我们写的程序,除了着色器这种运行在gpu中的特殊程序之外,其余代码都是运行在cpu的,可以看做是对于gpu进行传输数据并发令施号,所以这里也就可以将整个图形渲染管线看作一个服务端(server),我们写在cpu中运行的代码当做客户端(clinet),于是整个模型类似:

cb2dcd005e24b5ab1e0741907d1196b.png

我们一部分一部分地看:

1657982200764.png

可以看到,处于Client端的就是我们写的C/C++程序,这里主要是调用OpenGL的Api,向Server端传输指令。Server端主要指的是图形渲染管线。Client端和Server端是独立运行,互相不干涉,有点类似线程池,也有点上次MediaCodec中描述的猪肉餐馆的那味道。

1658027021981.png

在这里我们会通过OpenGL的Api向Server端,即图形渲染管线传递顶点数据、颜色数据、以及自定义的着色器需要的一些数据(比如含有变化的滤镜需要变化频率时间相关的参数)。相当于将加工手机的原材料传入了图形渲染管线的入口

因为OpenGL是一个没有感情的图形数据处理机器人,在传输数据的同时,我们会通过相应的指令告诉OpenGL要如何使用这些数据

通俗来说,就是我们把一堆原材料数据丢进OpenGL中,OpenGL就会启动图形渲染管线噼里啪啦将数据按照我的输入的指令进行加工,最后将加工好的图像数渲染到一个特定缓冲区中(帧缓冲,frame buffer),然后我们再调用指令,让缓冲区的数据渲染到屏幕上

这场景,是不是有点像我们小时候最熟悉的玩意...

e8e1fbe843b82fd4e7620989210bd0b7.jpeg

我们小时候也是不断发出指令,控制屏幕渲染出我们想要的一些效果。。

再看箭头旁边的单词,表示的是传递的数据,这个将在下一篇具体讲解着色器的时候细想叙述,这次我们先忽略。

1658025600406.png

接下来就是最令初学者兴奋又痛苦的着色器了:

aa47309585b4a7779b444aec58ebd50e.jpeg

1658027988699.png

这是图形渲染管线能够让我们尽情发挥想象力的地方,一旦掌握了它,你可能会对它如痴如醉。这里还是啰嗦一下,着色器这个重头戏是下一篇博文的主要内容,这里先卖关子不细讲了,这里你要知道的有2点:

1. 着色器是在我们传输入原始数据和指令之后,一个在gpu内部我们可以自由处理数据的地方。(又有点啰嗦,因为其实上面已经有提到了)

2. 顶点着色器输出的数据并不是直接传给片段着色器的,由上篇文章和上图可知,中间还有图元装配和光栅化,所以这里数据的传输会有一些微妙的关系,这也是很多初学者容易搞混的地方

1658027123297.png

状态机

我们已经大致了解了OpenGL的工作流程了,还有一个概念还需要掌握,那就是OpenGL状态机,很多初学者之所以看到OpenGL的代码一脸懵逼的原因之一,在于他们没有理解OpenGL是一个状态机,比如很多初学者看到OpenGL常见的以下代码内心是崩溃的:

unsigned int VBO, VAO; 
glGenVertexArrays(1, &VAO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). 
glGenBuffers(1, &VBO); 
glBindVertexArray(VAO); 
glBindBuffer(GL_ARRAY_BUFFER, VBO); 
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); 
 // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbindglEnableVertexAttribArray(0); 
 // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
 glBindBuffer(GL_ARRAY_BUFFER, 0); 
 glBindVertexArray(0);

看一遍之后,脑海中似乎只有一堆“bindXX”。。

32147a3a6835b7e02e3af5c6e5d7b1ab.jpeg

所以,首先,什么是状态机?查一下无所不知的百科:

状态机由状态寄存器和组合逻辑电路构成,能够根据控制信号按照预先设定的状态进行状态转移,是协调相关信号动作、完成特定操作的控制中心。有限状态机简写为FSM(Finite State Machine),主要分为2大类:

第一类,若输出只和状态有关而与输入无关,则称为Moore状态机

第二类,输出不仅和状态有关而且和输入有关系,则称为Mealy状态机

36f547d2b4335680e12ddab8dc3ca5f2.jpeg

还是讲点大众易懂的话把,举个栗子:

相信我们都坐过电梯吧,那么电梯有几个常见的状态呢,一般来说有以下几个:

开门关门运动(上升/下降)静止

它们有什么特点呢?

电梯只有静止的时候才能开门,只有开门之后才能关门,只有关门之后才可以运动,只有运动之后才可以静止,所以,可以说电梯的各个状态是有依赖关系的,换种更专业的说法,就是各种状态可以通过有向图来表示。

stateDiagram-v2


静止 --> 开门
开门 --> 关门
关门 --> 运动
运动 --> 静止

是的,电梯不能随意从一个状态跳转到另一个状态,总不能运动过程中开门吧。。

1658032138279.png

关于OpenGL状态机,Learn OpenGL中的叙述如下:

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。

再回到刚才这段代码:

unsigned int VBO, VAO; 
glGenVertexArrays(1, &VAO);
// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s). 
glGenBuffers(1, &VBO); 
glBindVertexArray(VAO); 
glBindBuffer(GL_ARRAY_BUFFER, VBO); 
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); 
 // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbindglEnableVertexAttribArray(0); 
 // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
 glBindBuffer(GL_ARRAY_BUFFER, 0); 
 glBindVertexArray(0);

其中各种BindXX方法调用是对某个事物(比如顶点数组对象、顶点缓冲对象等,这些概念将在后面博文会详细解读的内容)的绑定用状态机的方式去理解就是进入了某个状态,不过这里与电梯状态有所不同的是,它的状态是存在某种绑定关系的,某个时间段内一种状态会以类似属于另一种状态之中的方式存在。用状态图的方式来表示上面的代码就是:

stateDiagram-v2
[*] --> 绑定VAO(后面的操作都关联当前VAO)
解绑VAO(后面的操作都与当前VAO无关了) --> [*]

绑定VAO(后面的操作都关联当前VAO) --> 绑定VBO(后面的操作都关联当前VBO)
绑定VBO(后面的操作都关联当前VBO) --> 解绑VBO(后面的操作都和当前VBO无关)
解绑VBO(后面的操作都和当前VBO无关) --> 解绑VAO(后面的操作都与当前VAO无关了)

不过这里状态存在包含关系,因为一个VBO会被绑定于一个VAO中,所以用下图来看会更加直观:

1658054991067.png

通俗来说就是,执行了绑定XX到解绑XX之间的任何操作,都会影响到XX。~就像人一旦走进电梯之后到走出电梯之前这段时间的行为,都在电梯内执行,都可能会影响电梯。比如在电梯里面搞破坏。

1658056196172.png

所以代码不能单独几行来看,要看得更多更远,比如多看前后的bindxx。

经过这样的解释,是不是以后看到OpenGL各种bindXX的函数是不是豁然开朗许多?当然不止bindXX函数,各种状态设置函数都适用。(没看过的后面博文就能见识)

1658027123297.png

总结

今天的内容在我的博文属于偏少的,因为本文算是对上一篇文章轻松入门OpenGL ES——图形渲染管线的那些事的补充,通过对OpenGL客户端服务端的工作模型以及状态机的描述,让初学者们更进一步对OpenGL的工作机制有一个宏观的认识,为后面具体的编码环节打好基础。 下一篇文章:一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)

代码地址

(项目代码将不断更新)
github.com/yishuinanfe…

参考

《OpenGL规范文档》
learn opengl
《OpenGL超级宝典第五版》
《OpenGL编程指南第8版》

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

系列文章目录

体系化学习系列博文,请看音视频系统学习总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

一看就懂的OpenGL es教程