上节回顾

上一节讲述了如何通过 OpenGL ES 给 GPU 关联一套可以使用的 shader,这 一套 shader 是被放在一个 program 中当作一个整体供 GPU 使用的。那么 GPU 绘制图片不止是需要这套 shader,还需要给这套 shader 传递一些必要的输入参数, 比如想要绘制图片的顶点位置,形状,颜色等等信息,那么这一节,将学习如何通过 OpenGL ES API 把这些绘制所需要的信息传递给 GPU。


绘制所需要的信息

想要绘制一幅图片,最起码需要预先想好要绘制什么形状的图片,比如是绘制一个三角形还是一个圆形,这个图片大小有多大,以及这个图片应该是什么颜色的。所以需要通过 OpenGL ES 的 API 把这些信息,比如图片的顶点位置信息、顶点颜色信息等传给 GPU。那么这一节将详细讲解 OpenGL ES 中负责传入绘制信息的 API。


OpenGL ES API 详解

void glGenBuffers(GLsizei n, GLuint * buffers);

我们的目的是把数据传给 GPU,专业一点的说法就是把 CPU 对应内存中的数据传给 GPU 的内存中。比如在程序中分配一块内存,在这块内存中写入要传给 GPU 的东西,这块内存是位于 CPU 对应的内存中。那么传递的方式有两种, 一种是直接把 CPU 内存中的数据赋值给 GPU 中对应的变量,另外一种是把 CPU 中的数据打包,然后在 GPU 中创建一个 buffer object,将 CPU 中打包的数据传给 GPU 中的 buffer object,然后再在 GPU 中对这个 buffer 进行访问和使用。第二种方法虽然代码量会稍微多一点,但是会更加节省空间和时间消耗。之所以节约的原因有两个,第一,由于我们要传给 GPU 中的数据分为很多种,比如顶点坐标, 顶点颜色等,如果通过第一种方式传递,那么我们要进行多次传递,而从 CPU 到 GPU 的传递是耗时又耗资源,所以如果我们把 CPU 内存中的数据打包,一次性把数据全部传给 GPU 中的一个 buffer object 中,然后 GPU 只需要根据 buffer object 中数据偏移,就可以将该 buffer object 的某一块分配给某个变量,另外一块分配给另外一个变量了。原因二:可能一套顶点颜色会在多次绘制的时候,被 GPU 使用多次,比如先绘制一个三角形,要把顶点颜色传进去一次,然后又绘制一个颜色完全一样的三角形,那么又要把顶点颜色传进去一次,这样就需要传递两次数据,而如果用 buffer object,这个 buffer object 只要不被删除,则会一直存在于 GPU 中,那么 GPU 就可以多次使用这块 buffer 了。

编写程序要在保证功能的同时,注意对空间和时间的节约,所以一般都会使用第二种方法。而第二种方法,首先需要一个 buffer object。而 glGenBuffers 这 个 API,就是用于先创建 buffer object 的 name,然后再通过 API glBindBuffer 创建一个 buffer object。先说 glGenBuffers,glBindBuffer 这个 API 一会再进行说明。

这个函数的第一个输入参数的意思是该 API 会生成 n 个 buffer object name, 当 n 小于 0 的时候,出现 INVALID_VALUE 的错误。第二个输入参数用于保存被创建的 buffer object name。这些 buffer object name 其实也就是一些数字,而且假如一次性生成多个 buffer object name,那么它们没有必要必须是连续的数字。 buffer object name 是 uint 类型,而且 0 已经被预留了,所以肯定是一个大于 0 的整数。

这个函数没有输出参数。当创建成功的时候,会在第二个参数 buffers 中生成 n 个之前没有使用过的 buffer objects 的 name。然后这些 name 会被标记为已使用,而这个标记只对 glGenBuffers 这个 API 有效,也就是再通过这个 API 生成更多的 buffer object name 的时候,不会使用之前创建的这些 buffer objects name。 所以回忆一下,这一步其实只是创建了一些 buffer object name,而没有真正的创建 buffer object。而只有在这些 buffer object name 被 glBindBuffer 进行 bind 之后, 才会真正的创建对应的 buffer object。

void glBindBuffer(GLenum target, GLuint buffer);

上一个 API glGenBuffers 只创建了一些 bufferobject 的 name,然后在glBindBuffer 这个 API 再创建一个 buffer object。 这个函数的第一个输入参数的意思是指定 buffer object 的类型,就好像shader 分为 vertex shader 和 fragment shader 一样。buffer object 也是分类型的, 在 OpenGL ES2.0 中,buffer object 分为 VBO vertex buffer object 和 IBO index buffer object 两种。那么在这里,第一个输入参数必须是 GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER 。 其 中 GL_ARRAY_BUFFER 对应的是 VBO , GL_ELEMENT_ARRAY_BUFFER 对应的是 IBO。如果传入其他的参数,就会报 INVALID_ENUM 的错误。第二个输入参数为刚才 glGenBuffers 得到的 buffer object name。

这个函数没有输出参数,假如传入的 buffer 是刚被创建的 buffer object name,而且它还没有被创建和关联一个 buffer object,那么通过这个 API,就会生成一个制定类型的 buffer object,且与这个 buffer object name 关联在一起,之后指定某个 buffer object name 的时候,也就相当于指定这个 buffer object。新创建的 buffer object 是一个空间为 0,且初始状态为默认值的 buffer,初始状态为 GL_STATIC_DRAW。然后创建和关联完毕之后,也就会把这个 buffer object 当作是当前 GPU 所使用的 VBO 或者 IBO 了。如果传入的 buffer 已经有关联的 buffer object 了,那么只是把该 buffer object 指定为当前 GPU 所使用的 VBO 或者 IBO。然后 GPU 之前使用的 VBO 或者 IBO 就不再是处于被使用状态了。

所以回忆一下,通过 glGenBuffers 创建一些 buffer object name,然后通过 glBindBuffer,给 buffer object name 创建和关联一个 buffer object,同时,通过这个 API,还将参数 buffer 对应的 buffer object 设置为目前 GPU 所使用的 VBO 或者 IBO。虽然 GPU 中可以存放大量的 buffer object,但是同一时间一个 thread 的一 个 context 中只能有一个 VBO 和一个 IBO 是被使用着的。之后关于 buffer 的操作, 比如查询 buffer 的状态等,就会直接操作 VBO 或者 IBO,而不会在使用 buffer object name 了。所以,如果想使用某个 buffer object,必须先通过 glBindBuffer 这个 API,把这个 buffer 推出来,设置为 GPU 当前的 VBO 或者 IBO。

初始状态下 VBO 或者 IBO 都是与 buffer object name 为 0 的 buffer object 绑 定的,然而其实 buffer object name 为 0 的 buffer object 不是一个合法的 buffer object,所以如果当这个时候,对 VBO 或者 IBO 进行操作或者查询等,就会报 GL_INVALID_OPERATIO 的错误。

一个 buffer object 可以多次与不同的 target 进行绑定,即使重复绑定也没关系,因为 GPU driver 的开发人员会想办法去进行优化的。

当一个 buffer object 被 GPU 使用之后,任何对这个 buffer object 的操作都会影响其与 GPU 绑定的结果。比如一个 buffer object 被 GPU 使用之后,然后将该 buffer object 删除,那么原来本 context 中所有使用这个 buffer object 的 BO,就都会进行 reset,然后相当于与 buffer object name 为 0 的 buffer object 进行绑定, 也就相当于重新回到没有绑定任何 buffer 的状态;而其他 context 或者其他线程如果也使用这个 buffer object,那么删除的时候不会有任何改变,但是一旦使用了这个被删除了的 buffer,就会导致 undefine 的结果、GL Error、绘制中断甚至是程序终止。

当 VBO 与 buffer object name 不为 0 的 buffer object 绑定之后,也就说明了在 GPU 中对 shader 中的 attribute 赋值将通过 VBO,而非直接通过把 CPU 内存中的数据赋值给 GPU 中对应的变量了。通过 OpenGL ES 给 GPU 中正在使用的 shader 中的 attribute 赋值是通过 glVertexAttribPoint 这个 API,这个 API 一会会详细进行讲解,现在先简单的说一下。如果通过 VBO 给 shader 中的 attribute 赋值,则这个 API 的最后一个参数只需要传递一个偏移量即可,否则的话,则需要传入一个包含真正数据的数组或者指向真正数据的指针。可以通过 glGet 这个 API,参数 为 GL_ARRAY_BUFFER_BINDING 这个参数查询 VBO 绑定的 buffer object 的 name, 也可以通过 glGetVertexAttribiv , 参数为 GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING,进行查询。

当 IBO 与 buffer object name 不为 0 的 buffer object 绑定之后,也就说明了 通过 glDrawElement 这个 API 进行绘制的时候,则这个 API 的最后一个参数只需要传递一个偏移量即可,否则的话,则需要传入一个包含真正数据的数组或者指向真正数据的指针。这个 API 的其他功能,将在下一节进行说明。可以通过 glGet 这个 API,参数为 GL_ELEMENT_ARRAY_BUFFER_BINDING 这个参数查询 IBO 绑定 的 buffer object 的 name。

buffer object name 和对应的 buffer object 和 shader 以及 program 一样,都属于一个 namespace,也就是可以被多个 share context 进行共享。

void glBufferData(GLenumtarget, GLsizeiptr size, const GLvoid * data, GLenum usage);

创建了 buffer object 之后,就需要给这个 buffer object 赋予数据了,而 glBufferData 这个 API 就是通过 OpenGL ES,把 CPU 端保存的数据传递给 GPU 端, 保存在指定的 buffer object 中。

这个函数的第一个输入参数的意思是指定 buffer object 的类型,可以是 GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER,刚才已经说过了,在 bindbuffer 之后,对 buffer object 的操作,都会直接通过 target 进行操作,而不再通过 buffer object name 了(除非是对 buffer object 进行删除),由于一个 context 只能有一个 VBO 和一个 IBO,所以在这里通过 target 指定我们是操作 VBO 还是 IBO,就能精确的指定到实际操作的是哪个 buffer object。如果传入其他的参数, 就会报 INVALID_ENUM 的错误。第二个和第三个输入参数的意思是:data 是 CPU 中一块指向保存实际数据的内存,而 size 为 data 以机器单元为单位的大小,size 不能为负,否则会报 INVALID_VALUE 的错误。如果 data 不为 null,那么 data 中 的数据会被从 CPU 端 copy 到 GPU 端的 buffer object 中。如果 data 为 null,那么执行完这个 API 之后,依然会给 buffer object 分配 size 大小的内存,但是没有对这块内存进行初始化,也就是其中存储的将是 undefine。data 中保存的数值一定要按照 CPU 端的对齐要求进行对齐。第四个参数刚才在 bindbuffer 创建 buffer object 的时候提到过,指的是 buffer object 的 usage,也就是 buffer object 的数据存储方式 ,刚才说了,buffer object 的 usage 初始状态下为 GL_STATIC_DRAW。解 释一下什么是 GL_STATIC_DRAW。GL_STATIC_DRAW 意思就是程序暗示这个 buffer object 只会被赋值一次,然后在 GPU 中可能会多次读取调用。除了 GL_STATIC_DRAW 之外,还有两种 usage,分别是 GL_DYNAMIC_DRAW,意思就是程序暗示这个 buffer object 可能会被赋值多次,且在 GPU 中可能会多次读取调用。 以及 GL_STREAM_DRAW,意思就是程序暗示这个 buffer object 只会被赋值一次, 然后在 GPU 中也只会被读取调用少量的几次。usage 的用法只适用于 performance 的暗示,通过传入这个暗示,GPU driver 可以更好的选择存储 buffer object 的方 式和位置,更好的优化 buffer object 的读写效率。而 usage 并不影响使用的功能, 无论 usage 使用哪个,这个 buffer object 都可以被正常使用。如果传入除了这三个 enum 之外的其他参数,就会报 INVALID_ENUM 的错误。 这个函数没有输出参数,但是有以下几种情况会出错,除了刚才说的传入的target 或者 usage 不合法会报 INVALID_ENUM 的错,以及 size 为负会报 GL_INVALID_VALUE 之外,还有如果 target 没有对应一个合法的 buffer object,或 者说 target 对应的是不合法的 buffer object 0,那么会报 GL_INVALID_OPERATION 的错误。而且由于这里需要在 GPU 分配一大块内存,所以可能会出现内存不够的情况,而当内存不够的时候,会报 GL_OUT_OF_MEMORY 的错误。在这里解释 一下 GL_OUT_OF_MEMORY ,出现这个错误代码,说明了,GPU 中没有足够的内存去执行这个命令。这种错误,除了 GPU driver 中被打上了标记之外,还会导致 GL 的状态为 undefine。

如果在执行这个 API 之前 buffer object 中就存有数据,那么会先把之前的数据全部删掉,再把新的数据装入 buffer object 中。执行完这个 API,会将 buffer object 的状态更新,BUFFER_USAGE 设置为这个 API 传入的第四个参数, BUFFER_SIZE 会被设置为传入的第二个参数。

void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid * data);

这个 API 的功能和刚才的 glBufferData 类似,顾名思义,刚才那个 API 是给 buffer object 传入数据,这个 glBufferSubData 是给 buffer object 的一部分传入数据。

这个函数的第一个输入参数和 glBufferData 的第一个输入参数一样,用于指定 bufferobject 的类型,如果传入非 GL_ARRAY_BUFFER 或者 GL_ELEMENT_ARRAY_BUFFER 的参数,就会报 INVALID_ENUM 的错误。如果 target 没有对应一个合法的 buffer object,或者说 target 对应的是不合法的 buffer object 0,那么会报 GL_INVALID_OPERATION 的错误。 第二个和第三个和第四个输入参数的意思是:以 buffer object 的开始为起点,进行 offset 个位置的偏移,从这个位置开始,长度为 size 个单位的这么一块空间,使用 data 指向的一块 CPU 中的内存数据进行覆盖。offset 和 size 的单位都是机器单元。如果 offset 或者 size 为 负,或者 offset + size 大于 BUFFER_SIZE,那么就会出现 INVALID_VALUE 的错误。 data 中保存的数值一定要按照 CPU 端的对齐要求进行对齐。

这个函数没有输出参数。用于更新 buffer object 的一部分,或者是全部数据。 由于 buffer object 的空间已经通过 glBufferData 分配好了,所以在这里不会分配新的空间,也就不会出现 GL_OUT_OF_MEMORY 的错误。

有一点需要注意:通过这个 API 也可以覆盖 buffer object 的全部数据,只需要将 offset 设置为 0,size 设置为 GL_BUFFER_SIZE,data 指向一组新的数据即可。 如果我们想更新 buffer object 中的全部内容的时候,理论上也可以直接通过再调用一次 glBufferData 进行重新赋值。但是建议使用 glBufferSubData 来操作这样的事情。因为 glBufferData 会牵扯到内存的重新分配,这样也会比较耗费资源,而 glBufferSubData 则不会牵扯到内存的重新分配。

void glDeleteBuffers(GLsizei n, const GLuint * buffers);

当 buffer 不再被需要的时候,则可以通过 glDeleteBuffers 这个 API 把 buffer object name 删除。

这个函数输入参数的意思是该 API 会删除 n 个 buffer object,当 n 小于 0 的 时候,出现 INVALID_VALUE 的错误。buffers 保存的就是这些 buffer object 的变量名。如果传入的变量名为 0,或者对应的不是一个合法的 buffer object,那么 API 就会被忽略掉。

这个函数没有输出参数。当 buffer object 被删除之后,其中的内容也会被删掉,名字也会被释放,可以被 glGenBuffers 重新使用。如果被删除的 buffer 正在处于 bind 状态,那么就相当于先执行了一次 glBindBuffer 把对应的 binding 变成 binging 0,也就相当于什么都没有 bind 了,然后再进行删除。

void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name);

在说 GLSL 语法的时候介绍过 attribute,这个是 shader 中的一个存储修饰符, 只存在于 VS 中。OpenGL ES 可以通过 API 得到指定 VS 中某个 attribute 的位置, 然后通过这个位置以及另外一个 OpenGL ES API,将数据传输到 VS 中。主要是用于传入顶点坐标、颜色等信息的。所以 attribute 有一个 OpenGL ES 可以获取到的 location,然而这个 location 可以通过 OpenGL ES 的 API 直接指定,如果没有指定的话,就会在 linkProgram 的时候,由 GPU 给 shader 中所有开发者自定义,且没有被指定 location 的 active 的 attribute,分配一个 index。而 glBindAttribLocation 就是通过 OpenGL ES 的 API 给 attribute 指定 index,也可以说是指定 location。而 获取 attribute location 的 API 叫做 glGetAttribLocation,把数据传输给 Attribute 的 API 叫做 glVertexAttribPointer。现在先来说 glBindAttribLocation,而另外两个 API 一会再说。

这个函数的第一个输入参数为 program object,如果我们传入一个不合法的 program object 数值,就会出现 INVALID_VALUE 的 error。第二个参数为一个无符号数值,这个数值将会是这个 attribute 的 index 或者说是 attribute 的 location。 但是这个数值是有限制的,它不能大于或者等于 GL_MAX_VERTEX_ATTRIBS 的值, 否则会出现 GL_INVALID_VALUE 的错误,同时也会导致 link 失败。然而区别于 shader 或者 program 以及 buffer object 的名字,shader 或者 program 或者 buffer object 的名字为无符号的非 0 整数,然后 attribute 的 location 则可以是 0。第三个参数是一个字符串,用于保存 program 对应的 vertex shader 中的一个 attribute 的变量名。在这里暂时不会检测这个变量名是否真实有效,但是会检测这个变量名不能以 gl_作为前缀。在 GLSL 的语法中也说过,gl_作为前缀的变量名,是被 GLSL 语法预留的,自定义的变量不能使用 gl_作为前缀。如果使用了的话,则会 出现 GL_INVALID_OPERATION 的错误。GPU 会在执行这个 API 的时候把 name copy 过去了,也就是说,当执行完这个 API 之后,name 这个用于保存 attribute name 的字符就可以被 free 了。

这个函数没有输出参数。除了刚才说的那些错误情况,还有,假如给一个 matrix 的 attribute bindlocation,但是由于 matrix 需要不止一个 location,比如 mat4 就需要 4 个 location,而且是需要 4 个连续的 location,那么第二个参数 index 就是matrix第一列的location,index + 1就是matrix第二列的location,index + 2 就是 matrix 第三列的 location,index + 3 就是 matrix 第四列的 location,如果没有 4 个连续的 location,linkprogram 也有可能会失败。

这个 API 执行之后,会在下一次 link program 的时候生效,也就是,假如, 之前这个 program 已经被 link 一次了,attributeA 已经被自动分配了一个 location 1。然后再用这个 API 把 Attribute A 与 location 2 绑定,执行完这个 API 之后, attribute A 的 location 还是 1,使用 OpenGL ES API 去访问这个 attribute 依然要用 location 1 去访问。然而等这个 program 再次被 link 之后,attribute 的 location 才变成了 2。也就是说 program 再次被 link 之前,这个 API 的执行,在开发者看来是没有任何功效的。

我们知道这个 API 需要在 linkprogram 之前执行,那么其实它甚至可以在program attachShader 之前执行,也就是当 program 还没有关联的 shader 的时候 就可以执行了。因为我们执行这个命令的时候只会判断第三个参数是否有以 gl_ 作为前缀,并不关心它是否是 program 对应 shader 中一个真实存在,或者是否是一个 active 的 attribute。当然,如果第三个参数其实不是一个 active 的 attribute 的话,那么在 linkprogram 的时候,会直接忽略掉这次 bind 的。

这种 bind 属于 GL 的当前状态,也就是说它并不仅仅是一个 program 的行为。 比如我们对一个 program A 的 attribute A 绑定为 location 1,假如我们突然通过 glUseProgram 开始使用 program B 了,不再使用 program A 了,那么假如 program B 中也有 Attribute A,那么这个 Attribute A 的 location 也为 1,这个需要特别注意的。

程序也可以给多个 attribute bind 同一个 location,这种现象叫做 aliasing,但是前提是被使用的 program 的 shader 中,只有其中的一个 attribute 是 active 的。 否则就会出现 link error。compiler 和 linker 默认认为没有这种 aliasing,并也相应的做了一顶的优化。如果一个 attribute 被 bind 了两遍 location,那么前一遍就会失效。所以你没有办法给一个 attribute bind 两个 location。

不允许通过这个 API 去给内置的 attribute bind location,因为如果需要的话它们会被自动 bind 的。

GLint glGetAttribLocation(GLuint program, const GLchar*name);

无论是通过 glBindAttribLocation 给 attribute 指定一个 location,还是通过 linkprogram GPU 自动给 attribute 分配一个 location,总归 attribute 是有一个 location。那么 glGetAttribLocation 这个 API,就是 OpenGL ES 去获取指定 VS 中某个 attribute 的位置。获取到这个位置之后,才可以通过 OpenGL ES 的其他 API 对 这个 attribute 进行操作。

这个函数的第一个输入参数为 program object,如果我们传入一个不合法的 program object 数值,或者这个 program 没有被成功 link,那么就会出现 GL_INVALID_OPERATION 的 error。其实我原本也以为应该会出现 INVALID_VALUE 的错误,按照之前的惯例应该会出现 INVALID_VALUE 的错误,但是不管是 OpenGL ES 的 Spec 还是 khronos 的网站,都是说会出现 GL_INVALID_OPERATION 的错误, 那么这里大家要注意一下。第二个参数是一个字符串,用于保存 program 对应的 vertex shader 中的一个 attribute 的变量名。同样的,在这里暂时不会检测这个变量名是否真实有效,同时也不会检测是否以"gl_"作为前缀。所以在这里 name 只 需要是一个字符串即可。

这个函数有输出参数,如果 name 指定的 attribute 为这个 program 中的一个 active 的 attribute,则输出该 program 最近一次被 link 的时候该 program 中 name 指定的这个 attribute 的 location。如果 program 一共被 link 了两次,第一次 attribute 的 location 为 1,第二次 link 的时候 location 为 2,然后再执行一次 glBindAttribLocation 把 attribute 的 location 设置为 3.那么这个时候执行这个 API, 获取这个 attribute 的 location 就是 2。如果 attribute 为一个 matrix,那么则返回 matrix 第一列的 location。如果 name 以“gl_”作为前缀,或者指定的并非是一个 active 的 attribute,或者甚至是一个在当前 program 并不存在的 attribute,或者这个 API 执行的时候出现了以上说过的那些错误,那么就会返回-1。由于 0 也 是一个合法的 attribute 的 location,所以这里返回的是-1。

void glEnableVertexAttribArray(GLuint index);

获取了 attribute 的 location 之后,在 OpenGL ES 以及 GPU 真正使用这个 attribute 之前,还需要通过 glEnableVertexAttribArray 这个 API,对这个 attribute 进行 enable。如果不 enable 的话,这个 attribute 的值无法被访问,比如无法通过 OpenGL ES 给这个 Attribute 赋值。更严重的是,如果不 enable 的话,由于 attribute 的值无法访问,GPU 甚至在通过 glDrawArray 或者 glDrawElement 这 2 个 API 进行绘制的时候都无法使用这个 attribute。

这个函数的输入参数为 attribute 的 location,刚才说了,当 OpenGL ES 获取 到 attribute 的 location 之后,就可以通过这个 location 来对 attribute 进行操作。 但是这个数值是有限制的,它不能大于或者等于 GL_MAX_VERTEX_ATTRIBS 的值, 否则会出现 GL_INVALID_VALUE 的错误,正常的 attribute location 应该是小于这个值的,所以如果写了超过这个值的话,肯定是错误的。

这个函数没有输出参数。默认状态下所有开发者自定义的 attribute 都是 disable 的,如果想使用,一定要先 enable。

void glDisableVertexAttribArray (GLuint index);

有了 enable,就一定要有 disable。当绘制结束之后,就可以把没用了的 attribute 通过 glDisableVertexAttribArray disable , 将指定 program 中 的某个 attribute 的开关关闭。关闭后,在绘制的时候,GPU 就无法访问到 attribute 对应的值。 这个函数的输入参数也是 attribute 的 location,它不能大于或者等于 GL_MAX_VERTEX_ATTRIBS 的值,否则会出现 GL_INVALID_VALUE 的错误。

这个函数没有输出参数。

void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);

刚才做了那么多准备工作,获取到 attribute 的 location,然后 enable attribute, 目的就是往 attribute 传值。而最主要的传值 API 就是 glVertexAttribPoint,用处是 从 OpenGL ES 向 VS 中传输数据。在准备通过这个 API 传值之前,那些实际的值可能存放在 CPU 中,比如以一个指针或者数据的形式存放着,也有可能存放在 GPU 端,通过刚才创建并赋值了的 buffer object 保存着。不管存放在哪里,都可以通过这个 API 给 attribute 赋值。然后被赋值后的 attribute 就代表着若干个顶点的坐标或者颜色等等其他信息。

这个函数的第一个输入参数是 attribute 的 location,它不能大于或者等于 GL_MAX_VERTEX_ATTRIBS 的值,否则会出现 GL_INVALID_VALUE 的错误。第二个参数是 size,回忆一下说 GLSL 语法的时候说过的东西,当时说假如想绘制一个三角形,那么需要设定三个顶点,这个三个顶点都会经过 VS 的运算得到三个顶点真正的世界坐标值,然后对这三个顶点进行光珊化,会生成组成三角形的无数个顶点,这些顶点都会经过 PS,通过 PS 的运算得到这些顶点的颜色值。所以 VS 被执行了三遍,这三遍都是使用 VS 这个函数,不同的是这三次函数的输入不同, 导致输出结果不同。而 VS 这个函数的输入就是 attribute 等值。所以通过 glVertexAttribPoint 传给 attribute 的值,就是会分为多个单元,每个顶点认领一个单元,每个单元包含 size 个变量。size 默认为 4,也就是说如果想要绘制一个三角形,那么就需要传进来三个单元,乘以每个单元 4 个值,也就是要通过这个 API 传进来 12 个值。然后每个顶点的 attribute 依次认领 4 个值,这样三个顶点的 attribute 就准备好了。size 只能是 1、2、3、4,也比较容易理解,因为 GLSL 中的变量最大也就是 vec4 和 mat4。而如果是 mat4 的 attribute,还会根据列被拆分成 attribute,所以一个 attribute 最多也就 4 个分量,如果 size 没有使用这四个值其中的任何一个,就会出现 INVALID_VALUE 的错误。第三个参数是 type,用于指定存储的数据的数据类型,比如 GL_BYTE,GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_FIXED, 或者是 GL_FLOAT ,默认是 GL_FLOAT,如果 type 不是这些值中的一个,那么出现 INVALID_ENUM 的错误。第四个参数 normalized 意思是如果赋值给 attribute 的值为定点值或者整数值,在传递给 attribute,转化为 float 值的时候,是否需要被归一化,归一化就是带符号的值转化为(-1,1), 不带符号的值转化为(0,1)。true 的话为需要归一化,false 的话就是不需要归一化。一般我传递顶点或者颜色坐标都是直接使用归一化之后的值传递,这样就不需要归一化了,减少 GPU 的负担。第五个参数 stride 是间隔的意思,举个例子, 刚才为了画三角形,需要传入 12 个值。那么可以直接创建一个 12 个值的数组传入,也可以创建一个 15 个值的数组传入,其中第 5、10、15 这 4 个值为无用值, 第 1234、6789、1112131415 这 12 个值是有用的,然后把 size 写为 4,stride 写 为 5,GPU 就知道 5 个值为一个单元进行读取,然后前四个为有效值,使用前四个对 attribute 进行赋值。这就是 stride 的功能。如果直接创建 12 个值的数组传入,size 写为 4,stride 也为 4,或者 stride 可以为 0。因为为 0 的话,GPU 也知道先给第一个 attribute 赋值 size 个值,然后紧挨着的 size 个值赋值给第二个 attribute。stride 默认为 0,如果 stride 小于 0,则出现 INVALID_VALUE 的错误。 最后一个参数 pointer 非常重要,它分为两种情况,假如实际数据保存在 CPU 端, 那么 pointer 就是一个指向实际数据存放位置的指针或者数组地址。如果实际数据保存在 GPU 的 VBO 中,那么 pointer 就传入一个偏移,意思就是从 VBO 的某一位开始,从之后的那些数值读取 stride 或者 size 为一个单元,将 size 个数值为有效数据,顶点数个单元的值作为 attribute 的值。

这个函数没有输出参数。

void glVertexAttrib*f(GLuint index, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);

除了上面那个 API glVertexAttribPoint 用于给 attribute 赋值之外,还有一些简单的给 attribute 赋值的 API,这些 API 就是 glVertexAttrib*,*中包含三部分内容, 首先是一个数字,可以是 1 或者 2 或者 3 或者 4,第二个是个字母,可以有 f, 也可以没有 f,第三个是 v,可以有 v,也可以没有 v。先说这组 API 和刚才那个 API 的区别,刚才那个 API,我们针对三个顶点可以传入三组不同的单元,导致三个顶点的 attribute 都不同,而使用这组 API,所有顶点的 attribute 都一样了。 所以通过这个 API,指定了一个 attribute 之后,也就最多传入了四个值用于给所有顶点的 attribute 赋值。

通过刚才 API 的说明,知道了每个 attribute 最多由 4 个值组成,那么假如 attribute 由 1 个值组成,那么这里使用 glVertexAttrib1f 传入 1 个值,那么正好这个值就赋给了这个 attribute,但是假如 attribute 由 4 个值组成,而使用 glVertexAttrib1f 传入 1 个值,那么其他三个值就使用默认值,第二个和第三个为 0,第四个位 1;依此类推,如果使用 glVertexAttrib2f 传入 2 个值,那么其他两个值就使用默认值,第三个为 0,第四个位 1;如果使用 glVertexAttrib3f 传入 3 个值,那么其他一个值就使用默认值,第四个位 1。如果使用带 f 结尾的 API, 比如 glVertexAttrib4f,意思也就是传入的值为 float 类型,如果使用带 v 结尾的 API,比如 glVertexAttrib4fv,和使用 glVertexAttrib4f 唯一的区别也就是传入参数不同,使用 glVertexAttrib4f,需要传入四个值,使用 glVertexAttrib4fv,只需要传入一个包含四个值的指针即可。

这个函数的第一个输入参数是 attribute 的 location,它不能大于或者等于 GL_MAX_VERTEX_ATTRIBS 的值,否则会出现 GL_INVALID_VALUE 的错误。后面的输入参数,根据 API 的名称不同,传入的参数不同。总之是会根据 API 的变量名 传入对应的参数。

这个函数没有输出参数。使用这组 API,虽然会导致每个顶点的 Attribute 都一样,但是每个顶点一样可以支持 MAX_VERTEX_ATTRONS 个 Attribute。也支持 matrix 的 attribute。

GLint glGetUniformLocation(GLuint program, const GLchar *name);

在说 glLinkProgram 的时候,说过 linkprogram 的时候,会给没有指定 index 的 attribute 分配一个 location,也会把所有 shader 中开发者自定义的 active 的 uniform 初始化为 0,并且分配给其一个地址。而每次 relink 之后,uniform 的 location 就会被释放掉,然后再重新分配一个新的 location,当然新的 location 和 旧的 location 也有可能相同。而 glGetUniformLocation 这个 API 就是用于获取 uniform 的 location 的。

关于 uniform 的定义和用法,在 GLSL 语法中已经详细说过了,在这里再简单的讲几点,一是 uniform 是存在于 VS 和 PS 之中的,区别于 attribute 只存在与 VS 中,二是 uniform 针对不同的点值是一样的,区别于 attribute 针对每个点可以一样(比如用 glVertexAttrib*传入的时候),也可以不一样(比如用 glVertexAttribPoint)。三、uniform 值是只读的。四、若 compiler 和 linker 认为 unform 会被使用或者不确定是否会被使用,都会认为 uniform 是 active 的。五、它们的值一旦被 load 进来,program 开始 use 之后就一直保持着,直到 program relink。

这个函数的第一个输入参数为 program object,如果传入一个不合法的 program object 数值,那么就会出现 INVALID_VALUE 的 error。如果这个 program 没有被成功 link,那么就会出现 GL_INVALID_OPERATION 的 error。第二个参数是一个字符串,用于保存 program 对应的一个 uniform 的变量名。同样的,在这里暂时不会检测这个变量名是否真实有效,同时也不会检测是否以"gl_"作为前缀。 所以在这里 name 只需要是一个字符串即可,但是要注意,这个字符串不能包含空格。

这个函数有输出参数,如果 name 以“gl_”作为前缀,或者指定的并非是一个 active 的 uniform,或者甚至是一个在当前 program 并不存在的 attribute,或 者这个 API 执行的时候出现了以上说过的那些错误,那么就会返回-1。还记得 attribute 可以是 vec4 或者 mat4。而 unform 甚至可以是一个数组,一个结构体等等,这边不能将 name 设置为结构体、结构体数组或者 vector、matrix 的一部分, 以直接获取它们对应的地址,只能通过.或者[]的形式传入数组或者结构体中的一个成员,然后获取到该成员的地址。比如数组中第一个元素的地址就用数组名加 [0]来充当 name,来获取第一个元素地址。而数组的地址也就是它第一个元素的地址,或者直接把数组名通过 name 传入获取的地址。

void glUniform*iv(GLint location, GLsizei count, const GLint *value);

这个是给 uniform 进行赋值,有点类似 glVertexAttrib*,都是在获取到 location 之后进行赋值。但是又存在一定的区别。

glUniform*这一套 API 一共分为 3 种,根据 API 的名称不同,传入的参数不同。

第一种是直接以值的形式把数据传递给 Uniform,比如 glUniform1f, glUniform4f,glUniform4i 等。这个类型的 API 主要是用于当 shader 中的 uniform 为 float、int、vec 类型,且不是 array 的情况,给 uniform 赋值的。

这种 API 的第一个输入参数是 attribute 的 location,它必须是一个合法的 uniform 的 location 或者-1,否则会出现 GL_INVALID_OPERATION 的错误。如果 location 为-1,那么这个 API 就会被忽略掉,也不会改变任何 uniform 的值。

后面的参数根据 API 的不同而不同,比如 glUniform1f,它就一共只有 2 个参 数,刚才已经说了第一个参数,而第二个参数就是一个数值,这个数值就是用于 给 uniform 赋予的值。

而比如 glUniform4f,它一共就有 5 个参数,除了刚才所说的第一个参数,剩下的四个参数,就是用于给 uniform 赋予的值。

第二种是以指针形式把数据传给 uniform,比如 glUniform1fv、glUniform4fv、 glUniform4iv 等。这个类型的 API 主要是用于当 shader 中的 uniform 为 float、int、 vec 类型,而且允许 uniform 是 array 的情况,给 uniform 赋值的。

这个类型的 API 输入参数就都一样了。第一个输入参数也是 attribute 的 location,它必须是一个合法的 uniform 的 location 或者-1,否则会出现 GL_INVALID_OPERATION 的错误。

第二个参数和第三个参数的意思是,count 代表将要改变指定的这个 uniform 数组一共由几个数组元素组成,可以是把数组全部赋值,或者部分赋值,而 value 就是指向实际存放要赋予给 uniform 的值的地址。如果 count 大于 1,但是指向 的 uniform 却不是一个 array,则会出现 GL_INVALID_OPERATION 的错误,且对 unform 的赋值无效。如果 count 小于 0,则出现 GL_INVALID_VALUE 的错误。

比如 glUniform1fv,而指定的 uniform 是一个具有三个元素的数组,那么第二个参数 count 为 3,value 则是一个指向 3*1 个 float 值的一块内存的指针。

再比如 glUniform4iv,而指定的 uniform 是一个具有五个元素的数组,那么 第二个参数 count 为 5,value 则是一个指向 5*4 个 int 值的一块内存的指针。如果 value 指向一块更大的内存,那么超过 5*4 个 int 值之外的值就被忽略了。

第三种是以指针的形式把数据传给 uniform,比如 glUniformMatrix2fv、 glUniformMatrix4fv。它和第二种 API 的区别在于这个类型的 API 主要用于当 shader 中的 uniform 为 matrix 类型,它也支持 uniform 为 array。

这个类型的 API 输入参数也都一样了。第一个输入参数也是 attribute 的 location,它必须是一个合法的 uniform 的 location 或者-1,否则会出现 GL_INVALID_OPERATION 的错误。

第二个参数和第四个参数的意思是,count 代表指定的这个 uniform 一共由 几个数组元素组成,而 value 就是指向实际存放要赋予给 uniform 的值的地址。

比如 glUniformMatrix2fv,而指定的 uniform 是一个具有 3 个元素的数组,那么第二个参数 count 为 3,value 则是一个指向 3*2*2 个 float 值的一块内存的指针。

再比如 glUniformMatrix4fv,而指定的 uniform 是一个具有五个元素的数组, 那么第二个参数 count 为 5,value 则是一个指向 5*4*4 个 float 值的一块内存的指针。

第三个参数 transpose 必须为 GL_FALSE,意思就是指定给 matrix 赋值的时候, 是以列为单位进行赋值,第一列赋值完毕再赋值第二列。如果使用别的参数,就 会出现 GL_INVALID_VALUE 的错误,而且给 uniform 的赋值失败。

所以总结一下 glUniform*和 glAttrib*,区别一共有 3 个,1.attribute 针对每个点,值都可以不一样,而 uniform 针对每个点,值都一样。2.attribute 如果是 array,那么赋值也要以数字元素为单位进行赋值,一次只能赋值给一个数组元素, 而 uniform 为 array 的话,可以一次性把这个 array 中的值全部赋值了。3.虽然 attribute 和 uniform 都支持 matrix 类型,但是通过 glVertexAttribPoint 或者 glVertexAttrib 给 attribute 传值的时候,一次虽然可以传递多个单元,但是每个单元最多只能传递 4 个 float,而 glUniform 可以直接传入一个 matrix 类型的值。

在第一种和第二种 API 中,分别有一个 API 是用于给 sample 赋值的, glUniform1i 和 glUniform1iv,分别用于给 sample 以及 sample 数组赋值。我们知道 sample 是特殊的 uniform,相当于变量类型为 int 的 uniform,用于保存 texture object。如果通过其他 API,会因为 size 或者 type 不同,而出现 GL_INVALID_OPERATION 的错误。

刚才说到了 float 和 int 类型,但是如果 uniform 为 bool 类型,那么不管是用 glUniform*i 或者 glUniform*f 都可以。GL 会自动转化类型。如果输入参数为 0 或 者 0.0,则为 false,否则的话则为 true。

说了这么多了 glUniform*的 API,在传值的时候,一定要严格根据 uniform 的 尺寸和类型来选择适合的 API,假如 size 不匹配,比如 uniform 为 vec3,但是使用了 glUniform2fv,则会出现 GL_INVALID_OPERATION 的错误。而假如类型不匹配,除了 bool 的 uniform 会自动转化,其他都不会自动转化,比如 uniform 为 vec3, 但是使用了 glUniform3iv,则会出现 GL_INVALID_OPERATION 的错误。出现了这样 的错误,uniform 的赋值也会无效。

这个函数没有输出参数。除了出现以上所说的那些错误,还有,如果当前没有 program 被使用,那么则会出现 GL_INVALID_OPERATION 的错误。

这些 uniform 会保持这些赋予的值,直到 program 再次被 link,因为再次 link 的时候,这些 uniform 值又会被初始化为 0。

以上这些 API 就是 OpenGL ES 往 GPU 的 Shader 中传递 attribute 和 uniform 信息的主要 API。那么 OpenGL ES 中关于 attribute 和 uniform 的 API 除此之外, 还有很多 API 是用于查询服务的,比如 glGetBufferParameteriv 这个 API 是用于查询指定 buffer 的参数,查询的对象只能是当前被使用的 VBO 或者 IBO,查询的是 buffer 的 GL_BUFFER_SIZE 或者是 GL_BUFFER_USAGE。比如 API glIsBuffer 用于查询传入的参数,是否是一个合法的 buffer。比如 API glGetVertexAttrib 和 glGetActiveAttrib,是用于查询 attribute 的属性的,而 API glGetVertexAttribPointerv 是用于查询 attribute 的地址的。比如 API glGetActieUniform,是用于查询 uniform 的属性,而 API glGetUniform 用于查询指定 uniform 的值。

传入了顶点信息,准备好了shader,下面就可以使用API,触发GPU开始渲染了。

void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);

我们在EGL中,通过eglCreateWindowSurface的时候,创建了一块绘制buffer,这块buffer的大小为屏幕分辨率的大小。然而虽然这块buffer的大小已经确定了,但是并不代表我们使用这块buffer进行绘制的时候,一定要完全按照这个buffer的大小去绘制。就好比我们有一块A4的画纸,那么我们想画一个太阳和一间房子,我们会分两次去进行绘制,第一次会在画纸的左上角先绘制太阳,第二次会在画纸的右下角绘制一间房子。那么第一次绘制的时候,我们只要把绘制区域设定在左上角即可,而第二次绘制,即把绘制区域设定在右下角。而glViewPort这个API,就是用于设定绘制区域的。

设定绘制区域会对绘制出来的结果产生很大的影响,记得我们在通过glVertexAttribPoint给shader中的attribute传值的时候,假设这个attribute刚好是用于表示顶点坐标的变量。那么我们说到在传值的时候可以选择是否归一化,我也说过我一般传进去的值本身就是归一化好的值,对于顶点坐标,就好比我传入了一个(0.5, 0.5, 0, 1),这个点看上去就是画纸的中间点,然而假如我们的屏幕分辨率为1080*720,那么我们通过egl生成的绘制buffer的大小就是1080*720,然后假如我们通过glViewport设置的绘制区域为从画纸的左下角开始,宽高为1080*720。那么绘制区域就是整个画纸,而我们这个(0.5, 0.5, 0, 1)的点就是坐标为(540,360)的点。然而假如我们通过glViewport设置的绘制区域为从画纸的中间点开始,宽高为540*360。那么绘制区域就是画纸的右上角部分,而我们这个(0.5, 0.5, 0, 1)的点就是坐标为(810,540)的点。再举个例子,假如我们想要绘制(0.5, 0.5, 0, 1)这个点到(0.5, 1, 0, 1)这个点的一条线,而我们通过glViewport设置的绘制区域为从画纸的左下角开始,宽高为1080*720。也就是绘制区域就是整个画纸,那么我们画的这条线从(540, 360), 到(540, 720),长度为360个单位。假如我们通过glViewport设置的绘制区域为从画纸的中间点开始,宽高为540*360。那么绘制区域就是画纸的右上角部分,那么我们画的这条线从(810, 540), 到(810, 720),长度为180个单位。长度是刚才的一半。

所以绘制区域不同,直接导致了绘制的位置以及绘制图片的大小。

这个函数的前两个输入参数代表着绘制区域的左下角顶点在整个绘制buffer中的位置,初始值为(0, 0)。后两个输入参数代表着绘制区域的宽和长,初始值为屏幕分辨率的尺寸(所以如果没有调用这个API,则相当于调用了glviewport(0, 0, screen_width, screen_height))。后面两个代表宽和长的参数是有限制的,不能为负,否则会出现GL_INVALID_VALUE的错误。当然它们也不能太大,GPU中有对它们设定了最大值,可以通过glGet这个API,参数为GL_MAX_VIEWPORT_DIMS进行查询。如果超过了的话,GPU会自动对这个值进行clamp。当然GL_MAX_VIEWPORT_DIMS肯定会大于或者等于屏幕实际分辨率的尺寸的。

这个函数没有输出参数。被归一化之后的顶点坐标会根据输入参数计算出最终在屏幕上的顶点坐标。比如归一化之后的顶点坐标为(0.5, 0.5),绘制区域为从(0, 0)到(1080, 720),那么最终显示在屏幕上的点为0.5*1080 + 0.5 = 540.5,0.5*720 + 0.5 = 360.5。

通过以上的API,我们给绘制图片已经做了充足的准备工作,传入了需要传入的值,设定好了绘制区域,那么下面我们会说三个绘制API。

void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha);

glClear,用于清理绘制buffer。因为我们通过egl创建的绘制buffer,其实也就是一块内存,但是这个内存在刚被创建好的时候,并不会被初始化,那么根据平台不同,里面保存的东西可能不同,就好比我们虽然准备了一张画纸,但是这个画纸上面可能原本就有东西,这些东西是不确定的,所以我们先要把这张画图涂上底色,而我们可能希望这个画纸的底色是白色,也有可能希望是黑色的,或者其他颜色,而glClearColor这个API,就是设定一种颜色,作为清理绘制buffer所使用的颜色。

这个函数一共有4个输入参数,分别是rgba四个值,用于确定一种特定的颜色,供清理绘制buffer使用的。这4个输入参数初始值都为0。在输入的时候这4个值可以随意填写,但是会被闭区间[0,1]进行clamp。

这个函数没有输出参数,通过这个API确定的颜色,会被用于glClear,用于清理绘制buffer使用。

void glClear(GLbitfield mask);

这是我们将要学习的第一个绘制API,用于使用预先设定的值去清理绘制buffer,比如我们想要清理绘制buffer的颜色,那么通过刚才的glclearcolor API,我们已经确定好一种颜色了,然后通过这个API,就可以调用GPU,对绘制buffer的颜色根据刚才设定的颜色进行清理。

这个函数的输入参数为一个mask值,用处是指定绘制buffer中的哪一块被清理,因为绘制buffer中主要分为三块buffer,color buffer、depth buffer、stencil buffer。刚才我们已经学习了如何设置默认的清理绘制buffer的颜色,然后我们只要这里传入GL_COLOR_BUFFER_BIT,就可以使用那个颜色去清理绘制buffer了。而还有另外两个API,glClearDepth和glClearStencil,是用于设定默认的清理绘制buffer的depth和stencil值,然后在这里需要传入GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT,去清理绘制buffer的depth和stencil。如果传入除了这三个值之外的其他值,就会出现GL_INVALID_VALUE的错误。由于depth和stencil并非每个OpenGL ES程序都会需要的了,这些API我们将放在之后的课程再进行讲解。

这个函数没有输出参数。API会根据传入的参数去清理绘制buffer中的对应模块,比如清理color、depth、stencil模块,可以只清理一个模块,也可以通过一个API调用,清理多个模块。但是假如绘制buffer原本就没有包含该模块,比如很多GPU的格式是不支持stencil的,那么glClear stencil就会没有任何效果。还有很多别的GL状态会影响glclear的结果,比如scissor test,scissor的意思就是在绘制区域中再设定一个小的绘制区域,假如屏幕分辨率为1080,720,viewport为(0,0)到(1080, 720),然后当OpenGL ES开启了scissor之后,再通过glScissor设定小的绘制区域为(0,0)到(1,1),那么再执行glClear,就不会clear一整块绘制buffer,而是只会clear那一小块绘制区域。再比如glColorMask,glColorMask是限定了绘制buffer中的那些颜色分量可以被写,默认是四个颜色分量都可以写,但是也可以限定只能r通道可写,然后假如glClearColor设定为(1,1,1,1),也就是准备clear成白色,但是由于只有r通道可写,那么其实是会被clear成红色。初次之外,还有dither也会影响到glClear

glClear也会clear multisample的color/depth/stencil buffer。

void glColorMask(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);

当PS结束后,Mask可以限制color、depth、stencil是否可以写入对应的buffer。比如这个glColorMask API就是用于控制color buffer R\G\B\A通道的写入权限的。

这个函数一共有4个输入参数,分别是rgba四个值,用于确定RGBA四个通道是否有写入权限,如果传入的为GL_TRUE,则说明该通道有写入权限。初始状态下,默认RGBA四个通道都是可写的。当传入GL_FALSE的时候,所有的color buffers的R通道将都不可写。无法只控制某个通道的权限,要么就一次性控制四个通道。

这个函数没有输出参数。

void glDrawArrays(GLenum mode, GLint first, GLsizei count);

如果说刚才的glClear只是清理一下绘制buffer,虽然也是对绘制buffer的操作,改变了绘制buffer中的颜色等值,那么这个glDrawArray,就是一个使用我们传入的顶点信息,在绘制buffer中真正进行绘制的API。还记得我们在说attribute的时候,说过如果我们想绘制一个三角形,那么需要传入三个点的位置、颜色等信息。这些信息是会保存在attribute中的。所以通过对attribute的赋值,GPU那边已经保存了三个顶点的信息。然后我们就需要通过glDrawArray这个API传入参数,告知GPU,使用这三个顶点其中的哪几个顶点进行绘制,而且把用于绘制的点,如何进行图元装配。

先说这个函数的第二个和第三个输入参数。它们的意思是,通过这个API绘制,会使用到GPU中保存的那些顶点中,从第first个顶点开始,到first+count个顶点结束,使用count个顶点作为绘制的点。如果GPU中还保存有其他的顶点,那么那些顶点在这次绘制中就会被忽略掉。其中first和count不能小于0,否则会导致undefine的行为发生,且会产生GL_INVALID_VALUE的错误。这个函数的第一个输入参数为一个枚举值,用于指定通过这些顶点去绘制什么样的图元。可以是GL_POINTS,意思就是会把这些顶点在图纸上绘制成一个个的点,也可以是GL_LINE_STRIP, 意思就是一个连续的线段,第一个顶点作为第一条线段的起点,第二个顶点作为第一条线段的终点和第二条线段的起点,以此类推,第i个顶点作为第i-1条线段的终点和第i条线段的起点。最后一个顶点,作为之前一条线段的终点。如果只是用一个顶点去绘制,那么这个绘制API就毫无意义。这样,最终,绘制出来的要么是什么都没绘制出来,要么是一个线段,要么是一个折线。也可以是GL_LINE_LOOP,这样的话和GL_LINE_STRIP基本一样,唯一的区别就是最后一个顶点会和第一个顶点连在一起。 如果只是用一个顶点去绘制,那么这个绘制API就毫无意义。这样,最终,绘制出来的要么是什么都没绘制出来,要么是一个线段,要么是一个封闭的折线。 GL_LINES,也是用于绘制线段的,但是和刚才两个不同,这次绘制的线段不是连续的。指定这种格式之后,会把第1个和第二个点组成一个线段,第三个和第四个点组成一个线段,而这两条线段是独立的,依次类推,如果一共有2i或者2i+1个点,就会画出i个线段。可以看出,如果用于绘制的顶点数为奇数,那么最后一个顶点会被忽略掉。GL_TRIANGLE_STRIP,用于绘制三角形,会把第一第二第三个点连成第一个三角形,注意连接的顺序,123和321的连接方法,在图形学中是完全不一样的,图形学会通过这种顺时针还是逆时针来判断该图元为正面还是背面,之后会被用于剔除使用。而剔除这些特性属于并非每个OpenGL ES程序都会需要的了,这些API我们将放在之后的课程再进行讲解。 在这里我们先要知道连接的顺序很重要。所以在这里,是把123组成一个三角形,然后324再组成一个三角形,这个三角形与第一个三角形是有一条边是共享的,然后345再组成一个三角形,这个三角形与第二个三角形有一条边是共享的。所以通过这个格式,我们得到的是一堆共享边的三角形,且它们要么全部是顺时针,要么全部是逆时针的画法。如果只是用一个或者两个顶点去绘制,那么这个绘制API就毫无意义。 GL_TRIANGLE_FAN,也是用于绘制三角形,而且顾名思义,是要绘制成一个扇形三角形。那么会把123组成一个三角形,134组成第二个三角形,145组成第三个三角形,以此类推,把1,i-1,i组成最后一个三角形,而这些三角形也是都有一条边是共享的。 所以通过这个格式,我们得到的也是一堆共享边的三角形,而且它们要么全部是顺时针,要么全部是逆时针的画法。如果只是用一个或者两个顶点去绘制,那么这个绘制API就毫无意义。 最后一种格式GL_TRIANGLES。可想而知,也是用于绘制三角形,且绘制的是独立的三角形。也就是123组成一个三角形,456组成另外一个三角形。3i-2,3i-1,3i组成第i个三角形,假如绘制顶点的数量不是三的整数倍,那么多余的一个或者两个顶点,就会被忽略掉。这种格式,画出的三角形就不一定全部是顺时针或者全部是逆时针了。如果mode不是以上这些参数,则会出现GL_INVALID_ENUM的错误。

这个函数没有输出参数。由于绘制图片需要的不只是顶点坐标颜色信息,还需要顶点的法向量等信息,而这些信息经常使用默认值。所以,如果没有被赋值的这些信息,或者没有被通过glEnableVertexAttribArray enable的attribute,就会使用默认值去参与绘制。

如果当前被use的program不是一个合法的program,则绘制的结果为undefined。但是不会出现其他错误。

还有一种可能性会导致出错,我们已经知道了egl会给GL创建一块可以绘制使用的绘制buffer。其实OpenGL ES还可以自己创建一块绘制buffer,我们称之为FBO和RBO,关于FBO和RBO也有很多的API,但是由于这些API就并非每个OpenGL ES程序都会需要的了,这些API我们将放在之后的课程再进行讲解。而如果使用OpenGL ES自己创建的绘制buffer去绘制,但是假如OpenGL ES创建的绘制buffer有误,那么就会出现GL_INVALID_FRAMEBUFFER_OPERATION的错误。在这里我们解释一下GL_INVALID_FRAMEBUFFER_OPERATION,GL_INVALID_FRAMEBUFFER_OPERATION是最后一种glError的标记了,其他的我们之前都说过了,当OpenGL ES程序从一块没有complete的framebuffer中读取,或者往里面绘制的时候,就会报这个错误。而这种错误可以被忽略,除了这个API要执行的操作没有成功执行,以及GPU driver中被打上了标记之外,其他方面不会产生任何影响。关于这块framebuffer是否complete,可以通过glCheckFramebufferStatus这个API查询,如果查询的结果不是GL_FRAMEBUFFER_COMPLETE,则说明被查询的framebuffer不是complete的。

void glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid * indices);

这个API和刚才的glDrawArrays功能基本一样,唯一的区别就是选取绘制所使用的顶点的方式不同。假如GPU中一共保存有10个顶点的信息,那么通过glDrawArrays,我们会从这10个顶点中选取一个起点,然后再确定一个长度,所以取到的是连续的几个顶点。而glDrawElements更加灵活一些,开发者可以通过它指定一些不连续的顶点,所以需要传入一个数组,这个数组保存的就是顶点的index,比如我们可以传入一个1357,也就是选择第一第三第五第七个顶点,第一个绘制顶点为1,依次类推,用于绘制。GPU中其他的顶点,会在这次绘制中就会被忽略掉。

这个函数的第一个输入参数和glDrawArrays的第一个输入参数一样,用于指定将要绘制出来图元的mode种类。如果mode不是以上这些参数,则会出现GL_INVALID_ENUM的错误。刚才我们已经说的很详细了,这里就不详细说了。第二个输入参数和glDrawArrays的第三个输入参数一样,count,用于指定使用多少个顶点作为绘制的点。count不能小于0,否则会导致undefine的行为发生,且会产生GL_INVALID_VALUE的错误。最后一个参数 indices 非常重要,它分为两种情况,假如实际数据保存在 CPU 端, 那么 indices 就是一个指向实际数据存放位置的指针或者数组地址。如果实际数据保存在 GPU 的 IBO 中,那么 indices 就传入一个偏移,意思就是从 IBO 的某一位开始,从之后的那些数值读取 count 个数值作为 indices 的值。而第三个输入参数的意思是,存放顶点index内存中,所使用的变量类型为type。type必须是GL_UNSIGNED_BYTE或者GL_UNSIGNED_SHORT。否则则会出现 GL_INVALID_ENUM 的错误。

这个函数没有输出参数。错误的方式也和glDrawArrays基本一样。

如果当前被use的program不是一个合法的program,则绘制的结果为undefined。但是不会出现其他错误。

假如绘制buffer有误,那么就会出现GL_INVALID_FRAMEBUFFER_OPERATION的错误。

void glLineWidth(GLfloat width);

根据上面两个APIglDrawArraysglDrawElements的第一个参数mode,如果mode为GL_LINE_LOOP、GL_LINE_STRIP、GL_LINES。那么光栅化出来的就是线段,而线段的粗细就是由glLineWidth这个API设定的。

这个函数只有一个输入参数width,默认为1。如果小于或者等于0,则会出现GL_INVALID_VALUE的错误。

这个函数没有输出。实际上最终的线宽为floor(width),也就是说,如果传入为1.9,那么实际宽度为1。而如果传入为0.8,floor后为0,但是实际宽度依然取为1。如果 ∆ X> ∆ Y,那么width的线宽体现在列上,也就是纵向尺寸为width。反之,则体现在行上。

还有就是,支持多少尺寸的线宽,由硬件决定,只有尺寸1的线宽是强制要求必须支持的。其他尺寸是否支持,只能通过API glGet,传入GL_ALIASED_LINE_WIDTH_RANGE进行查询。实际上最终的线宽也会被clamp到这个支持的尺寸之内。

如果通过GL_LINE_WIDTH 查询,返回的则是glLineWidth这个API设置的数字,而非实际的数字(比如floor之后的数字)。

void glFrontFace(GLenum mode);

根据上面两个APIglDrawArraysglDrawElements的第一个参数mode,如果mode为GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN、GL_TRIANGLE。那么光栅化出来的就是三角形,绘制出来的结果同样也收到其他一些因素的影响,比如这个API glFrontFace以及glCullFace。这两个API会根据顶点的组合,决定剔除一些三角形不去进行绘制。可以把三角形分为front三角形和back三角形。而glFrontFace这个API就是用于算出哪些三角形为front三角形,而哪些为back三角形。

这个计算过程需要借用一个算法,其中x和y为第i个顶点的坐标,i⊕1为i+1 mod n,n为顶点的总数。i从0开始,到n-1结束。

opengles

这个函数只有一个输入参数 dir ,当dir为GL_CCW的时候,对上述算法取反,之后得到的结果如果为正,则该三角形为front。反之,则会back。而如果dir为GL_CW的话,则对上述算法的结果,直接判断其是否为正,如果为正,则该三角形为front。反之则为back。默认为GL_CCW。如果dir不是这两个值,则会出现 GL_INVALID_ENUM 的错误。

其实算法比较复杂,一般情况下GL_CCW的时候,逆时针的三角形为front,顺时针的三角形为back。GL_CW则相反。(Patrick:我觉得spec应该写反了,GL_CCW的话应该是顺时针为front)

这个函数没有输出。得到的三角形是否为front/back会和API glCullFace关联使用。

void glCullFace(GLenum mode);

由于back三角形一般都是不可见的,剔除这些不可见的三角形对性能会提高。所以一般会将这些不可见的三角形剔除掉。glCullFace这个API就是用于选择剔除哪些三角形。而剔除这个功能,则是通过glEnableglDisable这两个API通过传入 GL_CULL_FACE 来开关。

这个函数只有一个输入参数mode,mode可以为 GL_FRONT, GL_BACK, and GL_FRONT_AND_BACK。否则,则会出现GL_INVALID_ENUM 的错误。当传入GL_FRONT的时候,则剔除front三角形,当传入GL_BACK的时候,则剔除back三角形,当传入GL_FRONT_AND_BACK则全部剔除,只会绘制点和线段,无法绘制任何三角形。剔除功能只有GL_CULL_FACE被打开的时候生效,如果GL_CULL_FACE被关闭,则全部不剔除。默认情况下剔除功能是关闭的,剔除mode为GL_BACK。

截至到现在,OpenGL ES最重要的,每个OpenGL ES程序中都会使用到的API已经被我们说完了,比如如何创建和使用shader和program,比如如何给GPU传入attribute绘制信息,如何给GPU传入uniform信息,确定绘制区域以及启动绘制命令。

除了这些之外,还有很多一样也是很重要的API,比如这个课时我们提到的,通过OpenGL ES创建绘制buffer,通过API给GPU传递scissor、colormask、blend、depthstencil等信息,以及众多查询API和非常非常重要的一个模块,纹理等。这些API我们会在之后的课程中,进行一一介绍。

本节教程就到此结束,下一节将学习如何通过 OpenGL ES 创建和设置纹理信息,希望大家继续阅读我之后的教程。

谢谢大家,再见!


原创技术文章,撰写不易,转载请注明出处:电子设备中的画家|王烁 于 2017 年 7 月 13 日发表,原文链接(http://geekfaner.com/shineengine/blog9_OpenGLESv2_8.html)