XGundam05 发表于 2023-1-24 13:43

一看就懂的OpenGL ES教程——这或许是你遇过最难画的 ...

通过阅读本文,你将获得以下收获:
1.进一步理解图元装配
2.初步学习片段着色器的特点和使用上篇回顾

上一篇一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)主要讲解了着色器Shader,重点讲了顶点着色器,文末讲到顶点着色器处理完数据之后,载着数据的小马车将重新出发,根据之前一看就懂的OpenGL ES——图形渲染管线的那些事讲的内容,数据的下一站,就到了图元装配的阶段。


再探图元装配

此时故事的小马车已经到了第2个阶段:



之前在一看就懂的OpenGL ES教程——图形渲染管线的那些事 一文中介绍过图元装配阶段主要工作就是根据开发者的需要将顶点连接成为一个图形,比如如上图所示将三个点连接为一个三角形。
首先要解释清楚的一个概念就是图元(Primitive),什么是图元呢?
图元`这个东西容易误解为`图形`,图元确实和图形息息相关,但是`图元强调的是,用什么方式去将一系列点连接成为一个图形。比如对于只有1个点的情况,那么图元只有一种,那就是无法做连接操作,最终“连接”形成的图形就是一个点。
对于2个点的情形,可以选择为不连接,即形成2个点,也可以选择将2个点进行连接,形成一条线段。
对于3个点的情形,那情况就丰富了很多,可以选择不连接形成3个点,也可以只连接其中2个点形成一条线段和一个点。也可以将3个点连接起来,形成一个三角形。
C++音视频学习资料免费获取方法:关注音视频开发T哥,点击下方链接即可免费获取2023年最新C++音视频开发进阶独家免费学习大礼包!
那么对于3个以上的,那能提供的连接方式就多种多样了,OpenGL主要提供了以下的图元类型:



如果现在传入顶点着色器的顶点数组的元素分别为:v0,v1,v2,v3,v4,v5,v6,v7,那么对应的图元效果图如下所示:



那么怎么确定图元装配的类型呢?
如果看过前面2篇文章的童鞋,可能会注意到一个方法:
glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);这是OpenGL的绘制指令,在它前面的一系列指令只是配置,而glDrawArrays就像一个流水线开关,一踩下去,整个流水线才真正动起来,传入OpenGL的数据才像一条河流一样在一个个阶段流动起来,直到形成的图像数据被绘制到对应的帧缓冲~
该方法第一个参数就是指定上面说到的图元类型,这里传的是三角形条带GL_TRIANGLE_STRIP,第二个参数表示从传入的顶点属性数组的第几个元素开始绘制,第三个参数表示绘制多少个顶点属性数组元素。
关于图元详细信息,可以看下官网Primitive
如果对于图元还是有点模糊,没关系,后面会用实例一一讲解。让我们先进入另一个主角——片段着色器(Fragment Shader)。


片段着色器

其实看过之前的文章的老哥应该知道,图元装配之后,还会进行光栅化的操作,这个操作非常重要,它将装配好的图元切成一块块片段(fragment),而这一块块的片段,才为后面的着色提供了基础。



介于光栅化的内容主要是图形学算法相关的,本系列定位为入门,所以在这里就只要明白这个阶段的作用即可。而接下来的片段着色器,才是我们的主角,也是一个相当有意思的家伙。
【文章福利】免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTCrtmp hlsrtsp ffplay srs 等等)有需要的可以点击994289133加群领取哦~


作用特点

官网对于片段着色器的定义是:
A Fragment Shader is the Shader stage that will process a Fragment generated by the Rasterization into a set of colors and a single depth value.
The fragment shader is the OpenGL pipeline stage after a primitive is rasterized. For each sample of the pixels covered by a primitive, a "fragment" is generated. Each fragment has a Window Space position, a few other values, and it contains all of the interpolated per-vertex output values from the last Vertex Processing stage.
The output of a fragment shader is a depth value, a possible stencil value (unmodified by the fragment shader), and zero or more color values to be potentially written to the buffers in the current framebuffers.
Fragment shaders take a single fragment as input and produce a single fragment as output.提取核心内容就是:
1.片段着色器是通过对光栅化产生的片段数据的处理,产生一个颜色集合和一个深度信息。
2.每个光栅化产生的片段,会携带位置信息,以及顶点着色器产生的数据的插值信息。(什么意思呢,后面会详细讲到。)
3.片段着色器的输入是一个片段,然后将该片段携带一个深度值,若干个颜色值以及可能会有的模板值,然输出后传入对应的帧缓冲。(关于深度和模板,在 一看就懂的OpenGL ES——图形渲染管线的那些事一文的测试和混合章节已有提及)
一句话就是:片段着色器就是指定当前片段颜色的,如果需要的话,还可以指定深度、模板等信息供后面阶段处理。更通俗来说就是负责染色的。一个片段着色器代码是什么样子的呢?(基于GLSL3.0)
       #version 300 es
       precision mediump float;
       out vec4 FragColor;

      void main() {
         //给当前片段赋颜色值
         FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
      }
这是一个最基础的片段着色器,只做了一件事,就是接收上一个阶段传过来的vec4变量,然后赋给out变量FragColor表示该片段的最终颜色。这里在GLSL2.0中有个内置变量gl_FragColor表示该片段的最终颜色,但是在GLSL3.0已经被弃用。
第一行 #version 300 es不用解释了,还不清楚的请出门左转至 一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
第二行precision mediump float;表示精度修饰符,表示数据的精度,一般情况下,精读越高,渲染质量越高,不过性能开销就越大。这是专门为OpenGL es量身定制的修饰符,这完全体现了Khronos组织对于嵌入式设备性能的体贴和照顾。
精度修饰符的语法定义如下:
precision precision-qualifier type;
复制代码关键字precision,加上具体指定精度的修饰符precision-qualifier,加上具体的修饰类型type,便是它的全部。
precision-qualifier取值可以是highp, mediump, and lowp,精度从高到低。type取值暂时只有float和int。
因为这里着色器使用的是vec4类型,所以要指定为float,精度根据设备情况指定,这里我就风行中庸之道,指定为mediump。
下面一行是定义输出变量: out vec4 FragColor;
该输出变量将作为当前片段的颜色值传递到后续阶段(当然这不是片段的最终颜色值),它的类型是vec4,每个分量分别表示RGBA,并且颜色每个分量的强度设置在0.0到1.0之间。
接下来:
void main() {
         //给当前片段赋颜色值RGBA
         FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
      }
很好理解了,指定当前片段的颜色,之后这个片段渲染的时候大概率是这里指定的颜色值(如果不在后续阶段“搞事”的话)。


着色器的编译链接

如果看过上一篇博文一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二) ,这里就很好理解着色器的编译链接过程了。
没错,和顶点着色器的编译一样,并且它们可以用同一个着色器对象加载,从通过同一个着色器对象而被OpenGL使用。
着色器的编译链接依旧如下:
创建着色器对象着色器对象加载着色器代码编译着色器对象创建着色器程序关联着色器对象和着色器程序链接着色器程序使用着色器程序
所以完整的着色器加载代码如下:
//初始化顶点着色器对象
GLint vsh = initShader(vertexSimpleShape, GL_VERTEX_SHADER);
//初始化片段着色器对象
GLint fsh = initShader(fragSimpleShape, GL_FRAGMENT_SHADER);

//创建着色器程序对象
GLint program = glCreateProgram();
if (program == 0) {
    LOGD("glCreateProgram failed");
    return;
}

//向着色器程序对象中关联2个着色器对象
glAttachShader(program, vsh);
glAttachShader(program, fsh);

//链接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == 0) {
    LOGD("glLinkProgram failed");
    return;
}
LOGD("glLinkProgram success");
//激活着色器程序对象
glUseProgram(program);
绘制

接下来就和 一看就懂的OpenGL ES教程——再谈OpenGL工作机制中所讲的状态机相关了,此时OpenGL es程序处于已经处于激活着色器程序的状态,后面的操作都可以针对该着色器程序进行操作。
//清空屏幕和颜色缓冲
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//给顶点着色器传递顶点属性数组
GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPositio"));
glEnableVertexAttribArray(apos);
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, triangleVer);
//绘制三角形
glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
首先是清空屏幕和颜色缓冲,这里比较啰嗦,需要2行代码(感觉是不是一行代码就能搞定?):
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
1.第一行是指定将颜色缓冲清空为什么颜色,参数为对应的RGBA值。第二行为真正将颜色缓冲设置为glClearColor指定的值。可以说glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数
glClear方法的参数为指定要设置的缓冲区,可以传入的数值为:GL_COLOR_BUFFER_BIT、GL_DEPTH_BUFFER_BIT 和 GL_STENCIL_BUFFER_BIT,分别表示颜色缓冲、深度缓冲、模板缓冲。
关于这三个缓冲:
前面说过光栅化后形成的片段包含绘制一个像素的所有信息,包含颜色、深度、模板等,所有这些信息会缓存在3个缓冲区中,分别就是颜色缓冲(color buffer)、深度缓冲(depth buffer)、模板缓冲(stencil buffer)。2.接下来给顶点着色器传递顶点属性数组这部分在上一篇文章 一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)中已经有详细阐述,在这里就不赘述。
3.真正的绘制方法: glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
前面讲图元的时候已经讲过,它是真正启动整个图形渲染管线工作的按钮。
4.最后是交换缓冲区: eglSwapBuffers(display, winSurface);
在OpenGL程序中,会默认使用一个帧缓冲和应用程序窗口关联(我们也可以创建新的帧缓冲进行离屏渲染,后面章节会讲到),默认帧缓冲总会包含一个双重缓冲机制的颜色缓冲,所谓双重缓冲,就是一个直接在窗口显示的前置缓冲,以及一个用来渲染新图像的后备缓冲。
为什么要用双重缓冲呢?因为渲染图像是一个按照某个规律(一般是从左到右、从上到下)渲染每个像素,不是像孙悟空编程超级赛亚人那样一瞬间就把颜色贴上去的。想象下,如果一个缓冲即要显示同时又在渲染,会发生什么呢?闪烁感是难以幸免了,如果屏幕刷新率和渲染频率不一致的话,可能还会出现画面撕裂感,所以双缓冲的引入是非常必要的。
在这里eglSwapBuffers方法是在绘制指令处理完成,即图像已经渲染到后备缓冲之后,交换前后两个缓冲,将最新图像显示在屏幕上。
至于eglSwapBuffers传入的2个参数,在之前一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)EGL配置已经有描述,这里就不再赘述。



片段着色器的执行特点

既然片段着色器的主要作用是给当前片段赋值颜色,那么说明有几个片段就会执行几个片段着色器。假如当前渲染区域为800x600,则一共有480,000片段,假如帧率为30帧每秒,那么一秒钟要执行1,400,000个片段着色器代码!



因为着色器程序是执行在GPU中的,这就要从GPU和CPU的区别说起了。
对比下CPU和GPU的结构图(绿色的是计算单元,橙红色的是存储单元,橙黄色的是控制单元):
CPU:



可以看出CPU主要由强大的逻辑控制单元和存储单元以及计算单元,从图中可以看出计算单元只是占了CPU的一部分。
GPU:



然而GPU就很不一样了,逻辑控制单元和存储单元只是占了很小的一部分,计算单元占了绝大部分。
所以CPU擅长逻辑控制,串行的运算。GPU擅长的是大规模并发计算,计算量大,但没什么技术含量
一个很经典的比喻,CPU像一个教授,积分微分都会算。而GPU像很多个小学生。假如当前有一个任务,需要执行成千上万次没有依赖关系的一百以内加减乘运算除,那么派很多个小学生去完成比派出一个教授完成要合适的多。同理,由于每个片段着色器的执行都是独立的,所以片段着色器在GPU中是大规模地并发执行,即如下图所示:


(the bookof shaders)
所以之前文章说过,OpenGL是一个高效的渲染引擎的主要原因也就在于此~


总结

本文主要详细阐述了图元装配以及片段着色器的作用和执行特点,下一篇一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)博文讲重点去绘制图形,逐步接近完成我们的三角形绘制大业。
如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~代码地址

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

Shader官方文档Fragment ShaderCore Language (GLSL)你好,三角形《OpenGL编程指南(第8版)》

作者:半岛铁盒里的猫 链接:https://juejin.cn/post/7143614036046217230 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。在开发的路上你不是一个人,欢迎加入C++音视频开发交流群大家庭讨论交流!
页: [1]
查看完整版本: 一看就懂的OpenGL ES教程——这或许是你遇过最难画的 ...