OPENGL ES 3.0 学习总结

时间:2024-03-15 15:25:30

Opengl ES是opengl的一个分支,opengl es集中了opengl中高效的绘图功能部分,省去了低效繁杂的绘图功能部分,主要应用于手持和嵌入式设备的3D绘图,突出的优点就是处理快,消耗小。

Opengl就是一个状态机,只对当前绑定的纹理进行操作,如果需要对其他纹理进行操作,需要先解绑再绑定别的纹理。

  1. 创建绘图窗口

ESContest有一个类型为void*名为userData的参数,该参数就是专门提供给开发者使用,开发者将自己定义的参数全部放在userData中。

esCreateWindow ( esContext, "窗口名称", weigth, height, ES_WINDOW_RGB/*显示图像的格式*/ );

  1. 着色器

着色器对象和程序对象是使用opengl es的基础。

    1. 着色器对象

使用glCreatShader创建一个着色器,接着使用glShaderSource读取着色器,使用glCompileShader编译着色器,再使用glGetShaderiv检查着色器编译是否正确,以上这些步骤其实都已经集成在opengl内部库中,只需要调用esLoadShader便可以完成,但是我们实际操作过程中也不用esLoadShader,而是直接使用程序对象。

    1. 程序对象

通过esLoadProgram直接读取顶点着色器和片段着色器到programobject,后续直接对programobject进行相关操作即可;

  1. 顶点着色器

OPENGL ES 3.0 学习总结

3.1 #version 300 es

是使用openglES 3.0的标志,第一行必须写这个,系统读取顶点着色器时,会先根据第一行判断是那个版本,然后再读取下面的数据。

3.2 精度限定符:(顶点和片段着色器都有)

精度关键字有lowp、mediump、highp分别对应低、中、高精度;

其中为float指定精度则只作用于float类型的参数,int同理,这里只列出了float类型的精度:

precision mediump float;

precision highp float;

在顶点着色器中可以不写精度限定符,系统默认为highp(因为顶点着色器中通常都是顶点、位置、法线、纹理坐标等变换操作,必须要highp类型,而颜色计算、照明方式可以选用mediump)。

3.3 统一变量声明

uniform mat4 u_mvpMatrix;

uniform统一变量声明的标识符,这里声明一个4*4的MVP矩阵,即模型-视图-投影矩阵。

3.4 layout(location = 0) in vec4 a_position;

   Layout限定符,后面跟着的location对应的就是顶点属性的索引,顶点属性的索引的用法后面介绍;in:代表输入的意思;这里还隐藏了默认的插值限定符,默认的是smooth(平滑着色),即smooth in vec4 a_position,同时插值限定符还有flat(平面着色),centroid(质心采样),使用多重采样时,质心采样可以强制插值发生在被渲染图元的内部(否则图元边缘可能出现伪像)smooth centroid in vec3 v_color;

3.5 out vec4 v_color;

   这里out意思就是输出,但是这里的输出参数v_color是传递给片段着色器的,也就是说顶点着色器的输出都是片段着色器的输入;

3.6 main函数

   这里的main函数便是整个代码的入口,opengl会首先找到main函数,并从这里开始执行代码,而顶点着色器的输入变量需要在后续代码中输入;这里的gl_Position是内建特殊变量,用于输出顶点位置的裁剪坐标。注意代码最后的分号,顶点着色器和片段着色器都有这个分号!

  1. 片段着色器

OPENGL ES 3.0 学习总结

4.1精度限定符

第一行和顶点着色器一样,opengl es版本的说明,第二行精度限定符,片段着色器必须写明!!!

4.2 in vec4 v_color

这里片段着色器的输入对应于顶点着色器的输出

4.3 texture

   Uniform sampler2D s_tex;

Outcolor = texture(s_tex, v_color);

这里texture函数使用前面设置的纹理参数对应的颜色值进行采样;

  1. 顶点数组、顶点属性、缓冲区对象
    1. 顶点属性

使用glVertexAttrib*命令加载index指定的通用顶点属性,例如

GLfloat color[4] = { 1.0f, 0.0f, 0.0f, 1.0f };

glVertexAttrib4fv ( 1, color );

加载索引为1的顶点属性,这里就是color(这里的1必须和顶点着色器中layout后面的location对应)。

    1. 顶点数组

顶点数组指定每个顶点的属性,是保存在应用程序地址空间的缓冲区。

glVertexAttribPointer (0,3,GL_FLOAT,GL_FALSE,0,vertexPos );

这里的0对应顶点着色器中layout location后面的值,vertexPos对应的是顶点属性数据,这里只存放了位置信息,,而实际上可以同时存放位置信息,法线信息,纹理信息。使用完glVertexAttribPointer 函数,必须同时使用glEnableVertexAttribArray ( 0 )函数启用顶点属性数组,否则访问不到顶点属性数组的值,系统默认是关闭顶点属性数组的。

这里需要注意的是顶点数组中GL_FLOAT这个参数指定的是顶点属性数据格式,而数据格式决定了顶点属性数据的图形内存存储的需求,影响整体的性能,也是渲染所需内存带宽大小的一个因素;opengl es 3.0推出了GL_HALF_FLOAT的16位浮点顶点格式,建议使用该格式,而avm的opengl代码是在opengl es 2.0基础上搭建的,因此不支持该格式。

以下代码便是直接使用顶点数组完成绘图的代码,可以和使用缓冲区对象的代码对比着看,因为这里代码量很小,所以会觉得使用缓冲区对象比顶点数组麻烦很多,但是当代码量很大,绘图内容多的时候,使用缓冲区对象将会得到最佳性能,因此,推荐使用VBO。

OPENGL ES 3.0 学习总结

    1. 缓冲区对象(VBO

顶点数组数据经过上述的处理保存在客户内存中,我们需要绘图的时候,会将这些数据从客户内存中拿出来复制到图形内存中,完成绘制,而再需要重复绘制的情况下,则会导致多次复制顶点数据,从而影响整体的性能;缓冲区对象就是将这些顶点数据从客户内存保存到图形内存中,每次绘图直接使用便可,不需要重复的赋值调用,极大的改善了渲染的性能,降低了内存带宽的消耗。

Avm代码的opengl就是使用这种方法来完成的。

这里先介绍两个缓冲区对象:数组缓冲区对象(用于保存顶点数据)(GL_ARRAY_BUFFER),元素缓冲区对象(用于创建保存图元索引)(GL_ELEMENT_ARRAY_BUFFER);

OPENGL ES 3.0 学习总结

先调用glGenBuffers分配两个缓冲区对象名称,再调用glBindBuffer函数绑定缓冲区对象,再调用glBufferData函数存储具体的数据,其中data数据可以为NULL,表示保留的数据不进行初始化,若data不为NULL则将数据内容复制分配存储起来;

上面代码中if判断的内容可以和顶点着色器片段着色器一起放在初始化函数中,而真正循环操作绘图的时候,只需要调用下面的代码即可;这里需要注意的就是glEnableVertexAttribArray的调用必须和glVertexAttribPointer同步出现,以及glDisableVertexAttribArray的调用必须在绘完图像之后;

    1. 顶点数组对象(VAO)、

上面以及介绍了顶点数组和顶点缓冲区对象(VAO),很明显VAO优于顶点数组,因为使用VAO可以减少CPU和GPU之间复制的数据量,从而获得更好的性能。而opengl es 3.0引入了一个新特性,使顶点数组的使用更加高效:顶点数组对象(VAO)。VBO的使用需要多次调用glBindBuffer,glEnableVertexAttribArray,glVertexAttribPointer,为了更快的在顶点数组配置之间切换,我们可以使用VAO。

下面第一张图是初始化部分的代码,注意先初始化VBO,然后在调用glGenVertexArrays函数生成一个VAO对象,再进行glBindBuffer,glEnableVertexAttribArray,glVertexAttribPointer操作,相比单纯的使用VBO,只多了两个步骤就是生成VAO+绑定VAO;

第二张图是draw函数中,即在绘制图元的时候,只需要单纯的调用glBindVertexArray函数绑定当前的VAO为上述初始化中的VAO即可,再正常调用glDrawElements完成绘图;

这样做的好处就是可以极大的提升性能,减少CPU和GPU的使用,减少带宽负载,但是问题是顶点数据属性无法更改,一旦确定,则只能按照确认的顶点方式去绘制图元,配合glTexSubImage2D函数,可以实现大多数嵌入式设备的绘图要求;AVM代码使用的就是VBO配合glTexSubImage2D来完成的,我之前将原本的AVM代码中opengl部分升级到opengl es 3.0并使用VAO取代VBO完成绘图操作,但是实际测试发现两者处理时间差不多,有时候甚至是VBO快于VAO,这不符合书上所说的,目前不确定是什么原因,也许是opengl es 3.0的GPU加速没做,也许是部分代码没有正确使用,也许是glTexSubImage2D处理很耗时等等原因;

OPENGL ES 3.0 学习总结

OPENGL ES 3.0 学习总结

  1. 纹理

纹理操作是opengl的基本操作,opengl纹理的大小必须是2的次幂:2,4, 8, 16, 32, 64, 128, 256, 512,1024,是合法的,宽高可以是2x4, 16x128等等 ;没有纹理就没法绘图,opengl纹理的坐标是左下角为(0.0,0.0),右上角为(1.0,1.0);在(0.0,1.0)之外的坐标也是允许的,在该区域之外的纹理读取行为由纹理包装模式定义,具体的包装模式有GL_REPRAT、GL_CLAMP_TO_EDGE、GL_MIRRORED_REPEAT;

    1. 2D纹理

2D纹理是opengl中最基本和最常用的纹理形式,2D纹理就是一个图像数据的二维数组;一个纹理的单独数据元称为纹素(纹理像素),每个纹素根据基本格式和数据类型指定,这里的基本格式就是颜色深度等;

    1. 立方图纹理

立方图纹理其实就是由6个单独2D纹理面组成,比如天空图就是用六个面完成的贴图,其实很多游戏也是这么做的;

    1. 3D纹理

3D纹理可以看做2D纹理多个切片的一个数组,它用一个三元(s,t,r)坐标去访问;

    1. 纹理的使用

先使用glGenTextures生成指定数量的纹理,再使用glBindTexture将纹理绑定到GL_TEXTURE_2D,GL_TEXTURE_3D,GL_TEXTURE_2D_ARRAY, GL_TEXTURE_CUBE_MAP上,这里需要注意的是opengl es 2.0不支持GL_TEXTURE_3D,es 3.0才支持;一旦纹理绑定到一个特定的纹理目标上,纹理对象在删除之前就一直绑定在他的目标上;可以使用glBindTexture(GL_TEXTURE_2D, 0);来解除绑定;绑定成功之后,便开始真正的加载图像数据了,用于加载2D立方图纹理的基本函数是glTexImage2D,此外opengl es 3.0还可以使用多种替代方法指定2D纹理,包括不可变纹理(glTexStorageD)和glTexSubImage2D的结合,avm代码使用的便是glTexSubImage2D,为了的到最佳的性能,建议使用不可变纹理;

在最后Draw函数中,当需要绘制图元的时候,需要先调用glActiveTexture函数**纹理单元,再绑定纹理单元,最后完成绘图;

    1. Mipmap贴图

I.闪烁,当屏幕上被渲染物体的表面与它所应用的纹理图像相比显得非常小时,就会出现闪烁。尤其当相机和物体在移动的时候,这种负面效果更容易被看到。

II.性能问题。加载了大量的纹理数据之后,还要对其进行过滤处理(缩小),在屏幕上显示的只是一小部分。纹理越大,所造成的性能影响就越大。

Mipmap贴图可以完美的解决上述两个问题,mip贴图的思路是构建一个图像链---mip贴图链;mip贴图链始于原来指定的图像,后续的每个图像在每个维度上是前一个图像的一半,一直持续到最后达到链底部的1*1纹理,一个mip级别中的每个像素通常根据上一个级别中相同位置的4个像素平均值计算(盒式过滤);

    1. 过滤模式

纹理渲染时发生的两种过滤:缩小和放大;缩小发生在屏幕上投影的多边形小于纹理尺寸时,放大发生在屏幕投影的多边形大于纹理尺寸时;对于放大mip贴图并不起作用;

OPENGL ES 3.0 学习总结

这部分代码是avm的opengl初始化部分代码,在使用纹理的时候必须要经过上述代码处理,告诉opengl贴图的模式已经过滤处理;这里glTexParameteri的参数远不止上面几个,可自行百度查看;

    1. 加载3D纹理

使用glTexImage3D加载3D纹理或者2D纹理数组,一旦加载了纹理,就可以使用texture内建函数在着色器中读取该纹理;glTexImage3D这个函数opengl es 2.0中时没有的;

    1. 纹理子图像

用glTexImage2D上传纹理图像之后,可以更新图像的各个部分,可以使用glTexSubImage2D函数来完成;

OPENGL ES 3.0 学习总结

OPENGL ES 3.0 学习总结

上图1是AVM opengl初始化部分,调用glTexImage2D上传纹理图像,注意这里的图像为NULL,实际上传是在真正绘制图像的时候调用的glTexSubImage2D来完成的,也就是图2中的代码;

同样,3D也可以使用glTexSubImage3D来完成子区域图像的更新;

    1. 不可变纹理

应用程序使用glTexImage2D和glTexImage3D等函数独立的指定纹理每个mip贴图级别,这对opengl es驱动程序造成的问题是驱动程序在绘图之前无法确定纹理是否已经完全指定,也就是说,它必须检查每个mip贴图级别或者子图像的格式是否相符,每个级别的大小是否正确以及是否有足够的内存,这种绘图检查可能代价很高;而不可变纹理正是解决这个问题的。

不可变纹理的思路:应用程序在加载数据之前指定纹理的格式和大小,这样做,纹理格式变成不可改变的,opengl es驱动程序可以预先进行所有一致性和内存检查,一旦纹理不可改变,它的格式和大小就不会改变,但是任然可以通过glTexSubImage2D,glTexSubImage3D等函数渲染到纹理加载图像数据(将其作为帧缓冲区对象附着使用来实现);这个思路其实可以试着应用到avm代码中,毕竟目前的avm代码还是使用的glTexImage2D来指定纹理;

  1. 图元装配光栅化

7.1概念介绍

图元装配发生在顶点着色器处理图元顶点之后,在这一过程中,执行裁剪、透视分割、视口变换操作;光栅化是将图元转换成一组二维片段的过程,这些片段由片段着色器处理,代表可以在屏幕上绘制的像素;

Opengl es 3.0可以绘制的图元包括:三角形、直线、点精灵;三角形是应用程序渲染最常用的方法;点精灵通常用作粒子效果,比如雪花,光粒等;绘制图元的API有:glDrawElements、glDrawArrays、glDrawRangeElements、glDrawArraysInstanced、glDrawElementsInstanced,其中最常用的是glDrawElements和glDrawArrays,这两个当中用的更多的就是glDrawElements;

图元装配相关介绍和具体操作在另一篇文章中详细写了,这边就直接跳过;

 光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理(片段着色器的输入)。这些二维片段代表着可在屏幕上绘制的像素。用于从分配给每个图元顶点的顶点着色器输出生成每个片段值的机制称作插值(Interpolation)。这句不是人话的话解释了一个问题,就是从cpu提供的分散的顶点信息是如何变成屏幕上密集的像素的,图元装配后顶点可以理解成变为图形,光栅化时可以根据图形的形状,插值出那个图形区域的像素(纹理坐标v_texCoord、颜色等信息)。注意,此时的像素并不是屏幕上的像素,是不带有颜色的。接下来的片段着色器完成上色的工作。

7.2 剔除

剔除操作,glFrontFace指定正面三角形的方向,即绘制三角形是按照顺时针还是逆时针的方式,有效值为GL_CW(顺时针)或者GL_CCW(逆时针),默认值为GL_CCW;glCullFace指定要被剔除的三角形的面,有效值为GL_FRONT、GL_BACK、GL_FRONT_AND_BACK,默认值为GL_BACK,同时需要调用glEnable(GL_CULL_FACE)来启用剔除操作,系统默认是不开启剔除操作的;剔除操作应该始终启用,以避免GPU浪费时间和资源去光栅化不可见的三角形,可以提高性能;AVM代码中也用到了剔除操作;

OPENGL ES 3.0 学习总结

7.3多边形偏移

当绘制两个相互重叠的多边形时,可能会出现伪像,这个伪像被称为深度冲突伪像,是因为三角形光栅化的精度有限而发生的,这种精度限制可能影响片段生成的深度值的精度,造成伪像;

为了避免这种伪像,需要在执行深度测试和深度值写入深度缓冲区之前,在计算出来的深度值上添加一个偏移量;这里需要注意的是如果深度测试通过了,那么原始深度值将被写入到深度缓冲区中,而不是原始深度+偏移,所以需要在深度测试之前完成对偏移量的计算;

多边形偏移API调用:

glPolygonOffset(GLfloat factor, GLfloat units)

深度偏移 = m * factor + r * units

其中m是三角形的最大深度斜率,由顶点着色器计算得到,r是一个opengl es实现定义的常量,代表深度值中可以保证产生差异的最小值;多边形偏移需要使用glEnable(GL_POLYGON_OFFSET_FILL)来启用;

  1. 帧缓冲区对象(FBO)

帧缓冲区对象提供了渲染到纹理或者屏幕外表面的更好、更有效的方法,帧缓冲区的作用和用处很大,可以自行百度查看;

OPENGL ES 3.0 学习总结

帧缓冲区对象中只能有一个颜色、深度、模板附着,之所以是附着,是因为FBO可以附加多个缓冲区,可以灵活的在缓冲区中切换,上述Renderbuffer Object便是渲染缓冲区对象,具体的FBO介绍可以参考下面这边文章

https://blog.csdn.net/wangdingqiaoit/article/details/52423060

8.1 渲染缓冲区

glGenRenderbuffers创建渲染缓冲区对象,glBindRenderbuffer绑定渲染缓冲区对象,一旦绑定渲染缓冲区对象,就可以指定保存在渲染缓冲区中的图像大小和格式,可以使用glRenderbufferStorage完成上述操作,glRenderbufferStorage和glTexImage2D很相似,只是不提供图像数据;

8.2 帧缓冲区

   glGenFramebuffers创建帧缓冲区对象,glBindFramebuffer绑定帧缓冲区对象,可以使用glFramebufferTexture2D链接一个2D纹理作为帧缓冲区附着,这里的2D纹理目标由glTexImage2D创建;glFramebufferTextureLayer为连接3D纹理的一个图像作为帧缓冲区附着;

   下面是使用帧缓冲区对象和渲染缓冲区对象的代码示例,glTexImage2D中pixels参数为NULL,意思是我们将渲染到整个纹理区域,所以不输入任何数据;这里我们只连接了颜色缓冲区附着;屏幕外渲染缓冲区的宽度和高度不一定是2的次幂;

  

OPENGL ES 3.0 学习总结

OPENGL ES 3.0 学习总结

渲染到颜色和深度缓冲区代码

OPENGL ES 3.0 学习总结

8.3 性能提示和技巧

OPENGL ES 3.0 学习总结

  1. 结束语

这里只介绍了opengl es 3.0部分,opengl这个大家庭非常大,opengl es 3.0只是其中一小块,除此之外,还有诸如:

GLM:opengl的几何数学库;

GLUT:实用工具库;glut是opengl的跨平台工具库

EGL:opengl es的库;

GLU:实用库;glu是对gl的部分封装

GL:核心库;

 

文章参考OpenGL ES 3.0编程指南和各个博客!!