上节回顾

上面一节课,我们学习了一个OpenGL ES程序必须具备的一些API,从准备shader,到传入绘制信息,到最后的执行绘制命令。然而在上节课结束的时候,我们也提到了OpenGL ES除了这些必备的API之外,还存在一些别的模块。比如这节课我们要说的纹理。纹理,其实我们可以理解为是存在于GPU中的图片信息,是OpenGL ES中很重要的一个概念,也是游戏开发的重要组成部分。我们看到的绚丽的游戏界面,其实就是在一个个模型上,贴上纹理构成的。可以说游戏中的这些元素,它们的形状依靠的是顶点坐标,而色彩基本都是依靠纹理。那么这节课,我们主要对纹理进行介绍。


生成纹理前的准备工作

我们在上节课的时候学过buffer object的概念,说过buffer object其实就是对应GPU的一块内存,可以从CPU这边往GPU那边传输数据给buffer object赋值,每个buffer object也有自己的属性,比如GL_BUFFER_SIZE,usage等。而texture object和buffer object基本类似,也就是GPU中的一块内存,这块内存的数据也是从CPU端传入的,而CPU端的数据,一般都是解析一张JPG或者PNG等格式的图片获取到的。那么现在,我先介绍一下这些普通文件的数据内容,以及如何从中获取到我们生成纹理所需要的信息。

我们知道想要表示一个像素点的颜色信息,一般需要4个通道,RGBA,每个通道需要8位,用于保存一个0-255的值。所以想要表示一个像素点的颜色信息,如果不压缩的情况下需要32bit,也就是4byte,而我们的手机像素,比如iphone5,它的屏幕分辨率为1136×640,也就是说,一张原始尺寸为手机屏幕那么大的图片,所需要的空间为1136*640*4=将近300万byte,也就是将近3M,而我们可以猜测一个游戏中,随着切换场景,更换主角等,需要大量的纹理,也就是需要大量的图片资源,虽然不是每个图片资源的原始尺寸都需要像手机屏幕那么大,但是大量的图片资源,也会导致这需要大量的空间,而占用了大量的空间,也就影响到游戏的包体大小,在PC时代,游戏的包体影响还不是特别大,大型游戏的包体经常都是数GB,甚至更大。但是手机时代,一个手机,比如iphone的总存储空间也就最多128GB,而大多数手机的总存储空间也就几十GB,甚至十几GB。那么包体的大小就不能太大,再加上,大家下载游戏大多是通过Wi-Fi甚至是3G或者4G,追求的就是想快速下载下来,然后快速进行游戏,所以包体的大小有时候也会决定一个玩家的去和留。而且比如Google Store直接对包体的大小有限制。再加上现在手机的屏幕分辨率越来越大,那么对应的原始图片也需要越来越大,所以游戏的图片资源的大小,就是游戏开发者比较关注的一个地方。当然会有人说,那么手机是可以扩展SD卡的,这就好比电脑可以接一个外接硬盘一样,读写速度总归是没有自身存储空间的读写速度快。也会有人说,可以搞一个小的包体,然后用资源包的形式下载资源,那么第一次下载的时候包体就小了。但是这样的话玩游戏的时候总归是要下载资源包的,这样也就回到了玩家下载半天还没有下载好,影响去留的问题上。

所以缩小包体,一直是游戏开发者的一个关注点。缩小包体的一个重要方向,就是缩小纹理对应的原始图片文件的大小。所以这些原始图片通常压缩为PNG、JPG或者TGA格式,以占用更少的磁盘空间。而基本不会使用BMP这种无压缩的格式。

而想要生成纹理,第一步要做的就是读取这些压缩的原始图片,在CPU端将它们解压,然后由于传入GPU的时候需要用到OpenGL ES的API,所以解压之后还要转换成可以被OpenGL ES API接受的格式,才能保证纹理的正确生成。

首先,我们根据原始图片文件的绝对路径,去读取到这个文件中所有的数据,这里我们读取到的数据,包含了这个原始图片的颜色信息,也就是我们刚才计算的,一个图片的颜色信息是这个图片的像素点的个数*4那么大一块空间保存的信息,除了这些颜色信息,还会有这个原始图片的文件格式等信息。在这里都会读取出来。假如这个原始图片的文件中的内容不为空,下一步会根据原始图片的文件格式,调用不同的函数去解析原始图片中读取到的数据。由于每种格式的文件都是有着固定格式的,比如前多少位用于保存图片的格式(指明该文件的图片颜色信息中RGBA通道信息,以及每个通道的位数),再比如哪几位保存图片的宽和高,再比如哪一位开始保存的是图片的颜色信息等。知道了这些信息后,就可以根据图片的宽和高以及每个像素所占的空间,算出了图片中用于保存图片颜色信息的总长度,并且根据起始点位置,就可以从图片的数据中把图片的颜色信息完全取出来了。截至到现在,我们已经从这个原始图片文件中得到了我们想要的东西了,回忆一下,我们获取到了图片的宽高和颜色信息的格式,获取到了图片的颜色信息和保存颜色信息的这块空间。那么根据这些信息,我们自信已经可以生成纹理了。

从原始图片中获取到的这些信息我们可以称之为CPU端的内存,我们要用这块内存中的数据去生成一张GPU中的纹理。在CPU端,这组数据是按照行的顺序进行存放的,比如先存放了第一行第一列那个像素的信息,假如占据了1个byte,然后存放第一行第二列那个像素的信息,依次类推,那么假如原始图片的宽度为15,每个像素占据1个byte,那么在CPU端一行也就只有15byte。然后我们从这组数据中取数据去生成纹理的时候,也要按照这个顺序一一读取,然后把读取到的数据去GPU中构建一张纹理图片。那么问题就来了。由于在CPU中数据也是按照组存放的,有几行就有几组,按照刚才我们的假设在CPU端一行只有15个byte,假如我们读取的时候假如按照8对齐,那么先读了8个,再读8个,那么就读取了16byte的信息,也就出现了读越界的情况。所以,一定要设定好对齐规则,这个对齐规则可以理解成一次性读取CPU几个数据。那么在这里我们可以看到,我们会根据图片的宽度和CPU中每个像素占的位数相乘得到CPU中每行的bit,然后除以8,得到CPU中每行占据多少个byte。然后看看其能被多少整除。如果能被8整除,那么我们每次可以从CPU读取8byte的数据;当然也可能只能被1整除,比如图片宽度为15,每个像素占的位数为3byte,那么每行所占据的位数就是45byte,就只能被1整除。当然对齐的字节数越高,系统越能对其优化。所以使用纹理尽量使用POT,也就是power of two,2的幂次方作为宽度。

所以不管如何,我们在这里一定要设置好一个合理的对齐规则。而这个对其规则,是通过OpenGL ES API来设置的,glPixelStorei。

void glPixelStorei(GLenum pname, GLint param);

原则上说这个API是用于设置像素的存储模式了,其实我们可以理解为这个API是用于设置我们读写像素的对齐规则。

这个函数的第一个输入参数只能是GL_UNPACK_ALIGNMENT或者GL_PACK_ALIGNMENT。将客户端的颜色数据传输至GL服务端的过程称为解包unpack。相反,将服务器像素读取到客户端的过程叫做打包pack。我们刚才说的就是GL_UNPACK_ALIGNMENT,也就是将数据从CPU端解包出来的时候的对齐准则。而GL_PACK_ALIGNMENT则是将数据从GPU端读取出来的对齐准则。如果使用了其他参数,那么就会出现GL_INVALID_ENUM的error。第二个输入参数为一个整形数据用于指定参数的新的值。默认为4,可以设置的值为1、2、4、8。如果使用了其他值,那么就会出现GL_INVALID_VALUE的error。

这个函数没有输出参数。

那么截至到现在,通过一个原始图片,获取生成纹理的全部信息,所有的准备工作,我们就已经做完了,下面开始讲解,如何通过这些信息生成纹理。


生成纹理

void glGenTextures(GLsizei n, GLuint * textures);

刚才我们说过,texture object与buffer object类似。都是GPU中的一块buffer。我们也知道buffer object有glGenBuffers、glBindBuffer、glBufferData、glBufferSubData、glDeleteBuffers五大API,分别用于buffer object的创建,赋值和删除。同样的,texture object也有同样的五大API,分别是glGenTextures、glBindTexture、glTexImage2D、glTexSubImage2D和glDeleteTexture,功能与刚才buffer object的5个API基本一样。比如glGenBuffers这个API,就是用于先创建texture object的name,然后在glBindTexture的时候再创建一个texture object。我们先说 glGenTextures,而glBindTexture这个API一会我们再进行说明。

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

这个函数没有输出参数。当创建成功的时候,会在第二个参数textures中生成n个之前没有使用过的texture objects的name。然后这些name会被标记为已使用,而这个标记只对glGenTextures这个API有效,也就是再通过这个API生成更多的texture object name的时候,不会使用之前创建的这些texture objects name。所以回忆一下,这一步其实只是创建了一些texture object name,而没有真正的创建texture object,所以也不知道这些创建的texture的维度类型等信息,比如我可以先说在OpenGL ES2.0中texture可以分为2D和cubemap的,在OpenGL ES3.0还会有3D和2D_ARRAY的texture,这些就是texture的维度类型。而只有在这些texture object name被glBindTexture进行bind之后,才会真正的创建对应的texture object,texture才有了维度类型。

void glBindTexture(GLenum target, GLuint texture);

在上一个API,我们只创建了一些texture object的name,然后在glBindTexture这个API再创建一个texture object。

这个函数的第一个输入参数的意思是指定texture object的类型,就好像buffer object分为VBO和IBO一样。texture object也是分类型的,刚才我们已经说了纹理的维度类型,在OpenGL ES2.0中,分为2D texture和CUBEMAP texture 两种。2D texture比较容易理解,就是一张2D的纹理图片,CUBEMAP的texture顾名思义,是用于组成Cube的texture,我们知道cube是立方体的意思,那么立方体有6个面,所以CUBEMAP texture是由6张2D texture组成的。那么在这里,第一个输入参数必须是GL_TEXTURE_2D或者GL_TEXTURE_CUBE_MAP。其中GL_TEXTURE_2D对应的是2D texture,GL_TEXTURE_CUBE_MAP对应的是cubemap texture。如果传入其他的参数,就会报INVALID_ENUM的错误。第二个输入参数为刚才glGenTextures得到的texture object name。

这个函数没有输出参数,假如传入的texture是刚被创建的texture object name,那么它还没有被创建和关联一个texture object,那么通过这个API,就会生成一个指定类型的texture object,且与这个texture object name关联在一起。之后指定某个texture object name的时候,也就相当于指定这个texture object。texture object是一个容器,它持有该纹理被使用时所需要用到的所有数据,这些数据包括图像像素数据、filter模式、wrapmode等,这些数据我们一会再说。而且新创建的texture object,不管是2D texture还是cubemap texture,它们的状态都是根据维度类型,GL所初始化好的默认值,这个texture object的维度类型将在其生命周期中保持不变,一直到该texture被删除。创建和关联完毕之后,就会把这个texture object当作是当前GPU所使用的2D texture或者cubemap texture。但是有一点需要注意的是,当时我们说buffer object的时候,也说了,让创建和关联完毕之后,这个buffer object也是当前GPU所使用的VBO或者IBO了,但是texture会稍微复杂一点,texture的相关知识点中,还有一个叫做纹理单元的东西。纹理单元就好比一个容器,一个GPU中最多可以有8个纹理单元,具体有几个,需要看GPU的实现。纹理单元是用于盛放纹理的,一个纹理单元中同一个类型的纹理最多只能有一个,我们刚才已经知道了OpenGL ES中只有两种类型的纹理,2D texture和cubemap texture。所以一个纹理单元中最多只能有2个纹理。而GPU中同一时间一个thread的一个context中只能有一个纹理单元是处于active状态,而一个纹理单元中同一时间同一个类型只能有一个texture,所以,我们我们创建和关联一个texture,只是将该纹理关联到了目前active的那个纹理单元,然而刚好,该纹理单元是处于被GPU使用状态,所以才使得该texture是当前GPU所使用的2D texture或者cubemap texture。假设一种情况,我们这个texture依然属于这个纹理单元,没有被别的texture替换,但是这个纹理单元,不再被GPU使用了,那么该texture也就不再是当前GPU所使用的了。这层逻辑非常重要。也就是说,buffer object直接被GPU所使用,可以直接通过glBindBuffer提交给GPU。但是texture object则多了一层,texture object被纹理单元使用,而纹理单元被GPU使用,所以glBindTexture,只是将texture提交给了纹理单元。默认的纹理单元是GL_TEXTURE0,我们是通过glActiveTexture这个API来切换被使用的纹理单元,关于glActiveTexture我们一会再说。如果传入的texture已经有关联的texture object了,那么只是把该texture object指定为当前GPU所使用的纹理单元的2D texture或者cubemap texture。然后该纹理单元之前使用的2D texture或者cubemap texture就被从纹理单元这个容器中拿出来了,不再属于该纹理单元了。当然由于GPU正在使用这个纹理单元,所以这个texture object也就被顺便当作为当前GPU所使用的2D texture或者cubemap texture。这种操作在我们写OpenGL ES代码的时候经常使用,因为我们可能会有很多游离的texture,但是只有8个纹理单元。所以,如果要使用一个已经创建好的纹理,那么就需要先将其通过这个方式放入一个纹理单元中。还有另外一种方法,假如一个纹理单元中已经有了一个2D的纹理,但是我们想要用另外一张纹理,那么可以把已有的纹理中的内容修改成我们想要的内容,通过glTexImage2D/glTexSubImage2D,但是不建议这种方法,因为牵扯到了CPU到GPU的数据传输,占用了带宽,会比较耗费资源。而且还有一点需要注意的是,如果这个texture是2D的,那么target就不能再使用cubemap了,意思就是已经创建好的texture object不能修改类型,如果修改了的话,就会出现GL_INVALID_OPERATION的错误。

所以回忆一下,我们通过glGenTextures创建一些texture object name。然后通过glBindTexture,给texture object name创建和关联一个texture object,同时,通过这个API,还将这个texture object放入了当前被使用的纹理单元中,由于这个纹理单元正在被GPU使用,所以这个texture也就成了GPU所使用的2D texture或者cubemap texture,虽然GPU中可以存放大量的纹理单元,每个纹理单元又都包含着纹理,而且在纹理单元这些容器外面还有很多游离的纹理,但是同一时间一个thread的一个context中只能有一个纹理单元被使用,以及只能有一个2D texture和一个cubemap texture是被使用着的。之后关于texture的操作,比如查询texture的状态等,对texture进行赋值,我们就会直接操作2D texture或者cubemap texture,而不会在使用texture object name了。所以,如果想使用某个texture object,我们就需要先通过glActiveTexture active一个纹理单元,然后通过glBindTexture将texture放入该纹理单元,将该texture设置为GPU当前的2D texture或者cubemap texture,然后才能对该texture object进行操作。初始状态下每个纹理单元的2D texture或者cubemap texture都是与texture object name为0的texture object绑定的,然而其实texture object name为0的texture object是一个被预留的texture object。

一个texture object可以放在多个纹理单元中,而任何对该texture object的操作,都会影响到所有与其关联的纹理单元。比如删除一个2D的texture object,而这个texture object原本可能被放入0、1、4这3个纹理单元中。那么就相当于把这个纹理从这三个纹理单元中都取出来,也就相当于先把这三个纹理单元分别进行了active,然后进行了glBindTexture(GL_TEXTURE_2D,0)的操作。再比如对这个2D texture object进行修改,那么在使用这三个纹理单元中的GL_TEXTURE_2D的时候,使用到的都是修改之后的值。

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

void glActiveTexture(GLenum texture);

刚才我们已经说了,GPU中同一时间一个thread的一个context中只能有一个纹理单元是处于被使用状态,所以想要将所有纹理单元中都放入texture,就需要不停的切换active texture。那么glActiveTexture这个API,就是用于将某个指定的纹理单元设置为被使用状态。

这个函数的输入参数的意思是指定某个纹理单元被使用。GPU中最多可以有8个纹理单元,但是每个GPU又都不同,所以可以通过glGet这个API,传入GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS这个参数来获取到当前GPU最多可以有多少个纹理,GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS不能超过8。所以第一个输入参数只能是GL_TEXTUREi,其中i从0一直到GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -1。初始状态下,GL_TEXTURE0是被active的。如果这里传入的值并非GL_TEXTUREi,i从0到GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS -1。那么就会出现GL_INVALID_ENUM的错误。

这个函数没有输出参数。

OpenGL ES支持在一次绘制中使用多个纹理,这样可以增强画面表现力,也可以在PS中使用特定算法。这个也可以想象到,因为纹理在shader中只是一个普通的sample变量,或者说是只是一个普通的uniform变量,那么传入多个uniform是没有问题的,只要不超过uniform的限制即可。使用多重纹理的方式,也就是先通过glActiveTexture依次将纹理单元active,然后给各个纹理单元传入将要被用到的纹理对象,然后再把要用到的纹理单元,通过glUniform的API传入shader即可,然后在shader中就可以使用传入的若干个纹理了。但是需要注意的是,虽然一个纹理单元可以包含多个纹理,其中每个纹理格式对应一个纹理,但是在shader中,一个纹理单元只能使用其中的一个纹理。比如通过glUniform将纹理单元传给sample2D,那么在shader中只能使用这个纹理单元中的2D texture,而如果通过glUniform将纹理单元传给sampleCube,那么在shader中只能使用这个纹理单元中的Cubemap texture。

void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid * data);

创建了texture object之后,就需要给这个texture object赋予数据了,而glTexImage2D这个API就是通过OpenGL ES,把我们刚才准备好的数据,从CPU端保存的数据传递给GPU端,保存在指定的texture object中。

这个函数的第一个输入参数的意思是指定texture object的类型,可以是GL_TEXTURE_2D,又或者是cubemap texture6面中的其中一面,通过GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X, GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, GL_TEXTURE_CUBE_MAP_POSITIVE_Z, or GL_TEXTURE_CUBE_MAP_NEGATIVE_Z来指定,我们说了cubemap的texture其实也就是由6个2D texture组成的,所以这个函数实际上是用于给一张2D texture赋值。刚才我们已经说过了,在bindTexture之后,对texture object的操作,需要先active对应的纹理单元,然后再指定texture target来确定是哪个纹理,而不再通过buffer object name了(除非是对buffer object进行删除),由于GPU中同一时间一个thread的一个context中只能有一个纹理单元是处于被使用状态,而一个纹理单元最多只能有一个2D texture和一个cubemap的texture,所以在active了对应的纹理单元之后,在这里通过target指定我们是操作2D texture还是cubemap texture的哪一面,就能精确的指定到我们实际操作的是哪个texture object。如果传入其他的参数,就会报INVALID_ENUM的错误。第二个是指给该texture的第几层赋值。上个课时我们也简单的介绍过,没有mipmap的texture,就相当于只有一层mipmap,而有mipmap的texture就好比一层一层塔一样,每一层都需要赋值。所以在这里需要确认我们是给纹理的第几层赋值,绝大多数情况是给第一层赋值,因为即使纹理需要mipmap,我们也经常会使用glGenerateMipmap这个API去生成mipmap信息,而不直接赋值。glGenerateMipmap这个API一会我们再说。mipmap又称LOD,level 0就是第一层mipmap,也就是图像的基本层。如果level小于0,则会出现GL_INVALID_VALUE的错误。而且level也不能太大,因为texture是由最大尺寸限制的,而第一层mipmap就是纹理的原始尺寸,而第二层mipmap的尺寸为原始宽高各除以2,依次类推,最后一层mipmap的尺寸为宽高均为1。所以如果level超过了log2(max),则会出现GL_INVALID_VALUE的错误。这里的max,当target为GL_TEXTURE_2D的时候,指的是GL_MAX_TEXTURE_SIZE,而当target为其他情况的时候,指的是GL_MAX_CUBE_MAP_TEXTURE_SIZE。这里的GL_MAX_TEXTURE_SIZE和GL_MAX_CUBE_MAP_TEXTURE_SIZE,都可以通过glGet这个API获取到。而且还有,假如图像的宽或者高不是2的幂,那么有个专业术语叫做NPOT,non power of two。在OpenGL ES2.0中NPOT的texture是不支持mipmap的,所以针对NPOT的texture,如果level大于0,也就会出现GL_INVALID_VALUE的错误。第三个参数internalformat,第七个参数format和第八个参数type我们放在一起来说,就是指定原始数据在从CPU传入GPU之前,在CPU中的格式信息,以及传入GPU之后,在GPU中的格式。internalformat,是用于指定纹理在GPU端的格式,只能是GL_ALPHA, GL_LUMINANCE, GL_LUMINANCE_ALPHA, GL_RGB, GL_RGBA。GL_ALPHA指的是每个像素点只有alpha通道,相当于RGB通道全为0。GL_LUMINANCE指的是每个像素点只有一个luminance值,相当于RGB的值全为luminance的值,alpha为1。GL_LUMINANCE_ALPHA指的是每个像素点有一个luminance值和一个alpha值,相当于RGB的值全为luminance的值,alpha值保持不变。GL_RGB指的是每个像素点有一个red、一个green值和一个blue值,相当于RGB的值保持不变,alpha为1。GL_RGBA指的是每个像素点有一个red、一个green值、一个blue值和一个alpha值,相当于RGBA的值都保持不变。如果internalformat是其他值,则会出现GL_INVALID_VALUE的错误。第七个参数format和第八个参数type,用于指定将会生成的纹理在所需要的信息在CPU中的存储格式,其中format指定通道信息,只能是GL_ALPHA, GL_RGB, GL_RGBA, GL_LUMINANCE, and GL_LUMINANCE_ALPHA。type指的每个通道的位数以及按照什么方式保存,到时候读取数据的时候是以byte还是以short来进行读取。只能是GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_4_4_4_4, and GL_UNSIGNED_SHORT_5_5_5_1。当type为GL_UNSIGNED_BYTE的时候,每一个byte都保存的是一个颜色通道中的值,当type为GL_UNSIGNED_SHORT_5_6_5, GL_UNSIGNED_SHORT_4_4_4_4, and GL_UNSIGNED_SHORT_5_5_5_1的时候,每个short值中将包含了一个像素点的所有颜色信息,也就是包含了所有的颜色通道的值。从CPU往GPU传输数据生成纹理的时候,会将这些格式的信息转成float值,方法是比如byte,那么就把值除以255,比如GL_UNSIGNED_SHORT_5_6_5,就把red和blue值除以31,green值除以63,然后再全部clamp到闭区间[0,1],设计这种type使得绿色更加精确,是因为人类的视觉系统对绿色更敏感。而type为GL_UNSIGNED_SHORT_5_5_5_1使得只有1位存储透明信息,使得每个像素要么透明要么不透明,这种格式比较适合字体,这样可以使得颜色通道有更高的精度。如果format和type不是这些值,那么就会出现GL_INVALID_ENUM的错误。

同样的format在OpenGL ES2.0中,将对应相同的internalformat,比如format GL_RGBA就对应着internalformat GL_RGBA,format GL_ALPHA就对应着internalformat GL_ALPHA,这里一共有5种format,也对应着5种internalformat,分别是GL_RGBA,GL_RGB,GL_ALPHA,GL_LUMINANCE,GL_LUMINANCE_ALPHA。internalformat和format需要一一对应,而且确定了internalformat和format之后,type的选择也受到了限制,比如针对internalformat和format为GL_RGB的时候,type只能是GL_UNSIGNED_SHORT_5_6_5或者GL_UNSIGNED_BYTE。而internalformat和format为GL_ALPHA的时候,type只能是GL_UNSIGNED_BYTE。internal format、format和type必须要对应着使用。

第四个参数width和第五个参数height就是原始图片的宽和高,也是新生成纹理的宽和高。因为两者是一样的,图片信息以数据的形式从CPU传到GPU,可能每个像素点格式和包含的信息会发生变化,但是图片的大小,也就是像素点的数量,每行多少个像素点,一共多少行,这个信息是不会发生变化的,这里我们说的像素点其实在纹理的相关知识中还有一个专业术语叫做纹理像素texels,简称纹素。width和height不能小于0,也不能当target为GL_TEXTURE_2D的时候,超过GL_MAX_TEXTURE_SIZE,或者当target为其他情况的时候,超过GL_MAX_CUBE_MAP_TEXTURE_SIZE,否则,就会出现GL_INVALID_VALUE的错误。第6个参数border,代表着纹理是否有边线,在这里必须写成0,也就是没有边线,如果写成其他值,则会出现GL_INVALID_VALUE的错误。最后一个输入参数也是上面准备好的信息,意思是:data是CPU中一块指向保存实际数据的内存。如果data不为null,那么将会有width*height个像素的data从CPU端的data location开始读取,然后会被从CPU端传输并且更新格式保存到GPU端的texture object中。当然,从CPU读取数据的时候要遵守刚才glPixelStorei设置的对齐规则。其中第一个数据对应的是纹理中左下角那个顶点。然后第二个数据对应的是纹理最下面一行左边第二个点,依次类推,按照从左到右的顺序,然后一行完毕,从下往上再赋值下一行的顺序,一直到最后一个数据对应纹理中右上角那个顶点。如果data为null,那么执行完这个API之后,依然会给texture object分配可以保存width*height那么多像素信息的内存,但是没有对这块内存进行初始化,如果使用这个texture去绘制到图片上,那么绘制出来的颜色值为undefine。可以通过glTexSubImage2D给这块没有初始化的内存赋值。

这个函数没有输出参数,但是有以下几种情况会出错,除了刚才说的那些参数输入错误之外,还有如果target是CubeMap texture的一个面,但是width和height不相同,则会出现GL_INVALID_VALUE的错误。如果format与internalformat不匹配,或者type与format不匹配(比如type为GL_UNSIGNED_SHORT_5_6_5 但是format 不是GL_RGB,或者type 是 GL_UNSIGNED_SHORT_4_4_4_4 或者 GL_UNSIGNED_SHORT_5_5_5_1 而 format 不是 GL_RGBA),则会出现GL_INVALID_OPERATION的错误。

总结一下,这个命令的输入为CPU内存中以某种方式保存的像素数据,转变成闭区间[0,1]的浮点型RGBA像素值,保存在GPU中的texture object内。

一旦该命令被执行,会立即将图像像素数据从CPU传输到GPU的内存中,后续对客户端数据的修改不会影响服务器中的texture object相关信息。所以在这个API执行之后,客户端中的图像数据就可以被删掉了。

如果一个texture object中已经包含有内容了,那么依然可以使用glTexImage2D对这个texture object中的内容进行替换。

void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid * data);

这个API的功能和刚才的glTexImage2D类似,顾名思义,刚才那个API是给texture object传入数据,这个glTexSubImage2D是给texture object的一部分传入数据。这个命令不会改变texture object的internalformat、width、height、参数以及指定部分之外的内容。

这个函数的第一个和第二个输入参数和glTexImage2D的一样,用于指定texture object的类型,以及该给texture的第几层mipmap赋值。错误的情况也与glTexImage2D一样,target传入不支持的值,则会出现GL_INVALID_ENUM的错误,level传入了错误的数字,则会出现GL_INVALID_VALUE的错误。 第三个、第四个、第五个和第六个输入参数的意思是:以texture object的开始为起点,宽度进行xoffset个位置的偏移,高度进行yoffset个位置的偏移,从这个位置开始,宽度为width个单位高度为height的这么一块空间,使用data指向的一块CPU中的内存数据,这块内存数据的format和type为第七和第八个参数,将这块内存数据根据这块texture的internalformat进行转换,转换好了之后,对这块空间进行覆盖。OpenGL ES所支持的format和type刚才已经列举过了,如果使用其它值则为GL_INVALID_ENUM。如果xoffset、yoffset、width、height其中有一个为负,或者xoffset+width大于texture的宽,或者yoffset+height大于texture的高,那么就会出现INVALID_VALUE的错误。

这个函数没有输出参数。除了刚才那些因为参数问题导致的错误,还有,如果target指定的这个texture还没有被glTexImage2D或者glCopyTexImage2D分配好空间,或者texture的internal format要和传入的format与type不对应,则会出现GL_INVALID_OPERATION的错误。具体的对应关系刚才我们已经说了。glCopyTexImage2D是另外一种给texture赋值的方法,目的类似glTexImage2D,但是方式却不同,我们一会再介绍这个API。

void glCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);

我们上节课说过,在OpenGL ES中执行绘制命令,可以在绘制buffer的颜色buffer等buffer生成数据。那么我们想象一下,其实绘制buffer的颜色buffer,也就是一块方形的内存,里面保存了一块方形图像的颜色信息,由于绘制buffer是有格式的,这个我们在创建surface的时候就已经确定了绘制buffer中颜色buffer的格式,比如是存放RGBA中的哪些通道,每个通道各占多少位。刚才已经说了glTexImage2D是从CPU客户端把数据读取出来,需要读取的是数据、format、type、width、height,传输到GPU,遵循CPU的格式和GPU端的格式进行转换,在GPU中生成一个texture。而在绘制buffer中,我们也能获取到用于生成纹理的这些信息,所以glCopyTexImage2D这个API,就是直接从绘制buffer中读取一块数据,根据绘制buffer的格式和目标纹理的格式进行转换,在GPU中生成一个texture。它的用处也非常广泛,比如我们游戏中需要的图像,很多是不能或者不需要从CPU读取,比如我们在游戏中进行了拍照,然后在游戏中看我们拍的照片,那么其实就是从前一帧的绘制buffer中读取一些信息,保存在texture中,在后面的一帧,将这个texture绘制出来。

这个函数的前三个输入参数和glTexImage2D的一样,用于指定texture object的类型,该给texture的第几层mipmap赋值,以及将要生成纹理的格式internal format。错误的情况也与glTexImage2D一样,target传入不支持的值,则会出现GL_INVALID_ENUM的错误,level传入了错误的数字,则会出现GL_INVALID_VALUE的错误。internal format传入了不支持的值,则会出现GL_INVALID_ENUM的错误。 第四个、第五个、第六个和第七个输入参数的意思是:以绘制buffer左下角为起点,宽度进行x个位置的偏移,高度进行y个位置的偏移,从这个位置开始,宽度为width个单位,高度为height的这么一块空间,从这块空间中进行取值,取出来的值用于生成宽为width、高为height的一张纹理。width和height不能小于0,也不能当target为GL_TEXTURE_2D的时候,超过GL_MAX_TEXTURE_SIZE,或者当target为其他情况的时候,超过GL_MAX_CUBE_MAP_TEXTURE_SIZE否则,就会出现GL_INVALID_VALUE的错误。但是还有一种可能性,那就是这4个参数构成的方形,超过了绘制buffer的区域,那么在区域外读取到的值就是undefine。而如果width和height都为0,那么其实就是创建了一个NULL texture。最后一个参数border,代表着纹理是否有边线,在这里必须写成0,也就是没有边线,如果写成其他值,则会出现GL_INVALID_VALUE的错误。

这个函数没有输出参数,但是有以下几种情况会出错,除了刚才说的那些参数输入错误之外,还有如果target是CubeMap texture的一个面,但是width和height不相同,则会出现GL_INVALID_VALUE的错误。如果绘制buffer的format与internal format不匹配,则会出现GL_INVALID_OPERATION的错误,比如绘制buffer只有RGB通道,那么internalformat就不能使用含A通道的格式,因为根本没有读取到alpha信息。而当匹配的时候,会先将从绘制buffer中读取出来的数据扩展成RGBA四个通道,然后进行归一化,将数据clamp到闭区间[0,1],然后再转成internalformat的格式。我们之前在讲绘制命令的时候说过,绘制buffer可以是egl创建的,也可以是OpenGL ES自己创建的,其实用在这个地方,更多的是使用OpenGL ES自己创建的绘制buffer。那么问题就是,如果使用OpenGL ES自己创建的绘制buffer去读取数据生成texture,但是假如OpenGL ES创建的绘制buffer有误,那么就会出现GL_INVALID_FRAMEBUFFER_OPERATION的错误。

所以总结一下,glCopyTexImage2D和glTexImage2D总的来看,其实就是信息来源不同,其它基本一样。

void glCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);

这个API的功能和刚才的glCopyTexImage2D类似,顾名思义,刚才那个API是给texture object这个整体传入数据,这个glCopyTexSubImage2D是给texture object的一部分传入数据。这个API的目的类似glTexSubImage2D,不会改变texture object的internalformat、width、height、参数以及指定部分之外的内容。

这个函数的第一个和第二个输入参数和glTexSubImage2D的一样,用于指定texture object的类型,以及该给texture的第几层mipmap赋值。错误的情况也与glCopyTexImage2D一样,target传入不支持的值,则会出现GL_INVALID_ENUM的错误,level传入了错误的数字,则会出现GL_INVALID_VALUE的错误。 后面6个输入参数的意思是:以绘制buffer的左下角为起点,宽度进行x个位置的偏移,高度进行y个位置的偏移,从这个位置开始,宽度为width个单位高度为height的这么一块空间,使用这块空间的数据,去改变指定texture object的左下角为起点,宽度进行xoffset个位置的偏移,高度进行yoffset个位置的偏移,从这个位置开始,宽度为width个单位高度为height的这么一块区域的信息,它们公用了width和height这两个参数,原因也很简单,就是像素点是一一对应的,所以从绘制buffer中取多大区域的信息,就更改了对应texture多大区域的数据。如果width和height为0也没关系,只是这样的话这个命令就没有任何结果了。如果xoffset、yoffset、width、height其中有一个为负,或者xoffset+width大于texture的宽,或者yoffset+height大于texture的高,那么就会出现INVALID_VALUE的错误。

这个函数没有输出参数。除了刚才那些因为参数问题导致的错误,还有,如果target指定的这个texture还没有被glTexImage2D或者glCopyTexImage2D分配好空间,或者如果绘制buffer的format与internal format不匹配,则会出现GL_INVALID_OPERATION的错误。如果使用OpenGL ES自己创建的绘制buffer去读取数据生成texture,但是假如OpenGL ES创建的绘制buffer有误,那么就会出现GL_INVALID_FRAMEBUFFER_OPERATION的错误。

void glTexParameter*(GLenum target, GLenum pname, GLint param);

通过上面的API,我们已经生成了一张可以使用的纹理,并且,我们还知道了这张纹理的宽和高。下面我们要把这张纹理映射到绘制buffer上,那么映射的过程,需要用到一个新的概念,纹理坐标。顾名思义,纹理坐标,意思就是纹理的坐标,用于在纹理中限定一块区域,将这块区域,显示到绘制buffer的指定区域上。回忆一下OpenGL ES pipeline,通过之前的课程,我们创建了一套VS和PS,并传入了顶点信息,在VS中讲计算出各个顶点的位置,其实每个顶点还可以包含更多的值,比如顶点颜色、纹理坐标、法线信息、切线信息等,这里我们先说每个顶点对应着的纹理坐标,纹理坐标是用于指定该顶点对应纹理中的某个点。纹理坐标,我们又称之为UV坐标,以纹理的左下角为坐标原点,有两种度量形式:一个顶点在纹理中的纹理坐标通常用(u,v)表示,u和v的最大值分别是纹理的宽度和高度,通常由开发者来提供,通过attribute的形式将与顶点数量匹配的纹理坐标传入vertex shader,每个纹理坐标对应一个顶点,意思就是要将该顶点与纹理中的指定点对应,然后所有顶点确定好之后,在光栅化的时候,会对uv坐标进行归一化生成st坐标,取值为闭区间[0,1]。在光珊化的时候,生成像素点的顶点坐标,并使用插值smooth(或者非插值flat)的方法生成每个像素点的纹理坐标(颜色、深度等),使得屏幕上的点可以于纹理图片中对应。然后我们再将纹理以uniform sampler2D/samplerCube的形式传入PS,在PS中通过纹理坐标对纹理进行采样,得到的纹理中对应的颜色,映射到光珊化产生的像素点上,对像素点进行着色,作为一部分像素点的颜色信息,用这些颜色计算出当前像素点最终的颜色。当然也有的纹理是用于做为像素点的深度或者模板信息,但是最多情况,还是做为颜色信息。理解了纹理坐标以及其工作原理之后,我们来举个例子,比如,我们生成的这张纹理是一个头像,那么我们准备将这个头像显示在图片的左上角,作为主角的头像,点击这个头像,还可以看到放大版的头像。那么假如原本的纹理宽和高为64*64,而我们将头像放到左上角的时候,宽和高为32*32,然而点击放大的时候,头像的宽和高又变成了128*128。所以在这里这两种映射方式的纹理坐标是一样的,都是选定整张纹理去进行映射,但是对应的顶点位置以及由顶点位置确定的大小不一样。所以究竟是如何把一张64*64的纹理图片,映射成32*32和128*128的图片呢,这里就牵扯到了映射算法,而纹理映射有很多种算法,所以需要通过明确规定纹理属性的方法,确定映射算法。下面,我们将介绍纹理属性,同时,介绍纹理属性对应的映射算法。而glTexParameter*这个API,就是用于设置纹理属性的。

这个函数的第一个输入参数用于指定texture object的类型,必须是GL_TEXTURE_2D或者GL_TEXTURE_CUBE_MAP。用于指定当前active的纹理单元中的一张texture,也就是用这个target来确定设置哪张纹理的属性。所以想要修改一张纹理的属性,先要通过glActiveTexture,enable一个纹理单元,然后通过glBindTexture,把这个texture绑定到这个纹理单元上。然后保持这个纹理单元处于active的状态,再调用这个API,来修改指定纹理的属性。

第二个输入参数和第三个输入参数,用于指定修改纹理的什么属性,以及修改成什么值。其中,第二个参数有四种选择。可以是GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER, GL_TEXTURE_WRAP_S, or GL_TEXTURE_WRAP_T。

下面分别介绍这4种属性以及它们可以支持的数值:

第一个属性,GL_TEXTURE_MIN_FILTER是用于纹理被使用的那块区域的尺寸小于纹理尺寸,需要将纹理缩小的情况,针对这种情况,有六种映射算法。其中两种算法是直接将原始纹理上的一个点或者四个点拿去计算,得出映射点的值,另外四种算法需要借用到mipmap。mipmap刚才我们已经介绍过了,我们说过一个纹理,可以有多层mipmap,每层mipmap宽高以2倍速率递减,直到宽高均为1。一张纹理的mipmap可以通过glGenerateMipmap利用算法,根据纹理base level的值生成,也可以通过glTexImage2D、glCopyTexImage2D、以及glCompressedTexImage2D,给指定texture的指定level层赋值。这些API除了glCompressedTexImage2D,我们都在上面说过了,而glCompressedTexImage2D我们将在下节课讲纹理优化的时候进行解释说明。那么下面详细介绍一下GL_TEXTURE_MIN_FILTER对应的六种算法。

第一种是GL_NEAREST,就是直接取原始纹理限定区域中最接近的一个像素的信息,作为映射点的信息,举个例子,比如将一个5*5的纹理,整张映射到3*3的一张图片上,那么理论上,映射图片的中间点应该就是从纹理的中间点取值。

第二种是GL_LINEAR,就是根据原始纹理限定区域中最接近的四个像素的信息,计算出来一个加权平均值,作为映射点的信息,举个例子,还是比如将一个5*5的纹理,整张映射到3*3的一张图片上,那么理论上,映射图片左下角的点的值,应该就是根据纹理左下角四个点的值计算而来。

第三种是GL_NEAREST_MIPMAP_NEAREST,就是先选择一张与映射图片大小最接近的mipmap层,然后从这个mipmap层中,取最接近的一个像素的信息,作为映射点的信息,举个例子,比如将一个64*64的纹理,整张映射到4*4的一张图片上,我们知道64*64的纹理有很多mipmap层,第0层的宽高就是64,第1层的宽高为32,依此类推,第5层的宽高为4,正好与映射图片大小一致。那么理论上,就取这第5层的像素点的信息,直接对应到映射图片的各个点上即可。

第四种是GL_LINEAR_MIPMAP_NEAREST,就是先选择一张与映射图片大小最接近的mipmap层,然后从这个mipmap层中,取最接近的四个像素的信息,计算出来一个加权平均值,作为映射点的信息,举个例子,还是比如将一个64*64的纹理,整张映射到4*4的一张图片上,我们还是会选择第5层mipmap,然后理论上,映射图片左下角的点的值,应该就是根据这第5层mipmap左下角四个点的值计算出来的加权平均值。

第五种是GL_NEAREST_MIPMAP_LINEAR,就是先选择两张与映射图片大小最接近的mipmap层,然后从这两个mipmap层中,分别取最接近的一个像素的信息,计算出来一个加权平均值,作为映射点的信息,举个例子,比如将一个64*64的纹理,整张映射到5*5的一张图片上,我们就会取第4层和第5层mipmap,然后理论上,映射图片左下角的点的值,应该就是根据第4层mipmap左下角点的值和第5层mipmap左下角点的值计算出来的加权平均值。

第六种是GL_LINEAR_MIPMAP_LINEAR,这种算法最复杂。就是先选择两张与映射图片大小最接近的mipmap层,然后从这两个mipmap层中,分别取四个最接近的像素的信息,分别计算加权平均值,然后根据这两个加权平均值,再计算出来一个加权平均值,作为映射点的信息,举个例子,还是比如将一个64*64的纹理,整张映射到5*5的一张图片上,我们还是会选择第4层和第5层mipmap,然后理论上,映射图片左下角的点的值,应该就是根据这第4层mipmap左下角四个点的值计算出来的加权平均值与第5层mipmap左下角四个点的值计算出来的加权平均值,算出来的加权平均值。

需要注意的是,如果在shader中注明要使用mipmap,比如texture2D传入了第三个参数bias,那么GL_TEXTURE_MIN_FILTER一定要使用带mipmap的这后面四种映射算法。

在纹理的缩小函数中,基本都是将多个纹理中读出的点计算出一个映射图片上的点,所以走样的几率会偏低,但是由于也会丢失一部分纹素,而丢失的纹素可能包含重要的颜色过渡信息,那样就会导致贴图失真,造成锯齿,在游戏中表现为远景部分,由于物体受到近大远小的影响,对物体纹理进行了缩小处理,则会出现模糊。然而也可以想象到,这6种算法,肯定是效率和效果不能兼得,其中前两种不同过mipmap的GL_NEAREST和GL_LINEAR最快,它们只需要通过一张纹理图片上的点进行采样即可,但是GL_NEAREST更容易导致比例严重的失真,高分辨率的图像在低分辨率的设备上就会出现一些像素点跳跃比较大的情况,而GL_LINEAR在纹理缩小的时候,像素点过渡比较平滑,虽然会损失一些性能,但是效果会稍微好一点。而默认情况下GL_TEXTURE_MIN_FILTER对应的算法是GL_NEAREST_MIPMAP_LINEAR。

针对纹理的时候,纹素的丢失可能导致的图片锯齿问题,为了消除锯齿,有一门专门的图形学技术叫做反锯齿,英文叫做AA。在OpenGL ES中是通过多重采样技术实现的反锯齿。先说单重采样,比如我们根据纹理映射得到了一张映射图片,那么将映射图片传入绘制buffer的时候,单重采样会采用一一对应的方法,但是多重采样则会利用了多重采样缓冲区,由该点附近多个位置的颜色、depth、stencil的采样共同决定绘制buffer上一个像素点的信息,这样也就使得图片的边缘可以比较平滑的过渡,减小视觉上的瑕疵。生成每个像素使用邻近采样点的数量,数量越多,抗锯齿效果越明显,但相应的也会影响性能。这个最大数量受到硬件支持的限制,可以通过glGet函数,传入参数GL_MAX_SAMPLES来查询当前硬件支持的最大数量。多重采样只是针对多边形的边缘进行抗锯齿处理,所以对应用程序的性能影响比较小。在3D游戏的开发中,这个技术已经比较普遍了,但是在2D游戏中,由于大部分元素都是规则且垂直于摄像机的,所以锯齿现象不是特别明显,但是如果游戏中需要绘制一些不规则的线段或者多边形,则最好开启多重采样。

第二个纹理属性,GL_TEXTURE_MAG_FILTER,与GL_TEXTURE_MIN_FILTER相反,它是用于纹理被使用的那块区域尺寸大于纹理尺寸,需要将纹理放大的情况,针对这种情况,只有两种映射算法。可想而知,需要将纹理放大,那么mipmap这种比原始纹理还小的纹理就没有意义了,所以可使用的映射算法就是GL_NEAREST和GL_LINEAR。这两种算法和刚才一样,就不介绍具体如何映射的了。但是有一点需要注意,由于将纹理放大,那么之前纹理上的一个点可能就要对应映射图片上的多个点,这样很有可能就会出现大块的纯色区域。特别是GL_NEAREST算法,虽然比GL_LINEAR快,但是由于GL_LINEAR还是会使用相邻的四个点计算出来加权平均值,这样的话,映射图片上相邻的点颜色就不会完全一样,会有一个平滑的过渡,但是GL_NEAREST则直接使用一个个像素点,去生成一个个像素块。所以GL_LINEAR效果会明显比GL_NEAREST要好一些。GL_TEXTURE_MAG_FILTER对应的默认算法就是GL_LINEAR。

第三个和第四个纹理属性放在一起来说,GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T。这个属性与纹理坐标息息相关,默认我们理解纹理的uv坐标最大值就是宽高,st值就是[0,1]。但是,其实也有超过了[0,1]的情况,意思也就是想通过纹理坐标去纹理上取值,但是取到了纹理外面了,那么我们可以想象到,可以把纹理外面包一层,这一层的内容都是根据纹理的内容设置的,这个不是简单的把纹理拉大,而是把纹理外面套一层,套的这一层的内容就是根据这两个属性指定的算法计算出来的。这两个属性支持的算法都是只有三个,分别是GL_CLAMP_TO_EDGE, GL_REPEAT, 和 GL_MIRRORED_REPEAT。

将GL_TEXTURE_WRAP_S设置为GL_CLAMP_TO_EDGE的意思就是,假如s坐标超过范围[0,1],那么纹理外面套的那一层,横向部分,以纹理的边界值的颜色进行填充,假如纹理图片的边为黑色,内部为白色,那么会横向填充黑色。

将GL_TEXTURE_WRAP_S设置为GL_REPEAT的意思就是,假如s坐标超过范围[0,1],那么纹理外面套的那一层,横向部分,则对纹理图片进行复制,将横向完全填满。

将GL_TEXTURE_WRAP_S设置为GL_MIRRORED_REPEAT的话,与设置为GL_REPEAT类似,假如s坐标超过范围[0,1],那么纹理外面套的那一层,横向部分,则对纹理图片进行镜像复制,将横向完全填满。

GL_TEXTURE_WRAP_S都是进行横向填充,GL_TEXTURE_WRAP_T则是进行纵向填充。GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T的默认算法都是GL_REPEAT。

这个API的三个参数的值上面已经全部列举出来了,特别是第二个和第三个参数要搭配使用,如果用错,则会出现GL_INVALID_ENUM的错误。

这个函数没有输出参数。如果GL_TEXTURE_MIN_FILTER设置的是需要使用mipmap的四个算法之一,但是纹理为NPOT的纹理,又或者纹理的mipmap不是通过glGenerateMipmap,而是通过glTexImage2D、glCopyTexImage2D、以及glCompressedTexImage2D,生成的,但是没有给需要的level赋值或者赋值的格式不对,那么就相当于本次绘制是用了一张RGBA为(0,0,0,1)的纹理。而如果纹理为NPOT的话,但是GL_TEXTURE_WRAP_T、GL_TEXTURE_WRAP_S没有使用GL_CLAMP_TO_EDGE,也就相当于用了一张RGBA为(0,0,0,1)的纹理。

总结一下,刚才我们一共说了纹理的三个属性,第一个属性,缩放,也就是假如纹理坐标对应的纹理区域与映射区域大小不同,需要对纹理进行放大或者缩小的时候,设定相应的映射算法。设定好的算法,可以平衡好采样的效率和效果,所以开发者在设置的时候需要根据自己的需要进行设置。

第二个属性,wrapmode,假如纹理坐标st使用到了超过[0,1]的坐标,那么针对横向和纵向,对纹理的外层进行填充,设定相应的填充算法。

第三个属性,mipmap,也就是给纹理对象设置一系列的小纹理,用于当映射区域小于纹理坐标限定的纹理区域大小的时候,可以借助小纹理中进行采样。尽管纹理的filer属性可以用于处理适当的纹理缩放,但远远不能满足图形应用程序的需求。由于纹理可能会经过远大于2倍的缩放,那么就会很容易造成失真,而由于移动设备的分辨率差异很大,所以不同设备使用同一个分辨率的资源,就会很容易出现缩放超过2倍的情况发生。

mipmap的功能分两层,第一层就是会在进行缩小的时候,提高采样效率,因为从一张小纹理进行采样肯定会比从一张大纹理中采样要速度快,效果说不定还会比大纹理采样的结果好,因为生成mipmap的时候,我们可以通过glHins选择GL_NICEST的方式,这样小纹理不会特别的失真,(下面我们会再介绍glHints这个API)。然而如果使用大纹理再进行GL_NEAREST的话会更容易失真。当然有人可能说那生成mipmap是需要耗费资源的,其实大部分mipmap都是在离线生成好,然后在游戏不忙的时候将资源传入,生成纹理的,再不济也是会选择在游戏不忙的时候使用glGeneratemipmap生成,虽然一定程度上影响了应用程序的性能,但是减少了对GPU内存和带宽占用,且只需要generate一次,尽量不会让生成mipmap成为游戏性能的瓶颈。第二层功能,就是有时候显示小图片的时候其实是使用不一样的内容,这样的话就需要mipmap是通过glTexImage2D等API传入的,而非glGeneratemipmap生成的,这样的话开发者就可以控制texture的第i层mipmap的内容,比如可以让mipmap第0层显示人物的全身照片,而第1层显示人物的大头照。这样,把纹理绘制到绘制buffer上的时候,根据纹理坐标大小,导致同一张纹理绘制出来的结果不同。所以mipmap是非常有用的,它只有一点不好,那就是如果离线生成mipmap内容的话,会导致包体要稍微大一些,每张纹理大概会比原来多占用1/3的空间。

void glGenerateMipmap(GLenum target);

这个API已经被提起很多遍了,我们再总结一下。首先,先介绍一下mipmap。Mipmap又称多级纹理,每个纹理都可以有mipmap,也都可以没有mipmap。这个概念我们在上面有接触过,当时我们说了有mipmap的texture就好比一层一层塔一样,每一层都需要赋值。纹理texture object就是GPU中一块内存,这块内存中保存着一定宽高的颜色信息以及其他属性信息。而一个texture object中可以包含不止一块内存,mipmap的texture object就包含着多级内存的。比如,我们创建的texture object的宽和高为32*32,那么我们知道,当纹理被准备好的时候,会拥有一块可以存放32*32个像素点颜色信息的内存。如果我们通过命令使得texture object包含多级内存,第一级内存就是刚才那块保存了32*32个像素点颜色信息的内存,而第二级内存就是保存了16*16个像素点颜色信息的内存,依次类推,每降低以及,宽和高都将缩小一倍,一直到第六级内存就是保存了1*1个像素点颜色信息的内存。也就是说,宽高为32*32的纹理,如果生成多级纹理,就会多出5块内存,大小分别是16*16,8*8,4*4,2*2,1*1。当生成多级纹理之后,我们使用texture object name指定的texture object,就是这个包含了多级纹理的纹理。多级纹理的用处一会我们再说,我们先说多级纹理是如何生成的。我们在说glTexImage2D这个API的时候,说过纹理的内存是通过这个API生成的,当使用这个API的时候,第二个输入参数就是制定给纹理的第几层赋值,当时我们都是给第0层赋值,那么现在我们就知道了,第0层为纹理的base level,那么默认都是给第0层赋值,但是我们也可以通过这个API给纹理的第1层mipmap,第2层mipmap赋值,一直到第N层mipmap。而这个在给第i层mipmap赋值的时候顺便也就把需要的内存生成出来。我们也说过每个纹理的mipmap是有限制的,比如32*32的texture只能有6层mipmap,而64*64的texture有7层mipmap,依次类推。但是通过这个方式,一次只能给一层mipmap赋值,将多级纹理的所有层都赋上值,需要调用好多次命令。所以就有了glGenerateMipmap这个函数,这个函数就是将一个base level已经准备好的纹理,生成它的各级mipmap。这两种方式唯一的区别在于,通过glTexImage2D赋值的时候,对应texture object对应的内存存放的值是由开发者指定的,开发者可以往里面随意存入数值,而通过glGenerateMipmap这个函数生成的多级纹理中存储的值,第i层的值都是直接或者间接根据第0层的值计算出来的。生成算法一会再说。

这个函数的输入参数用于指定texture object的类型,必须是GL_TEXTURE_2D或者GL_TEXTURE_CUBE_MAP,来指定当前active的纹理单元中的一张texture,也就是用这个target来确定生成哪张纹理的mipmap。所以想要对一张纹理生成mipmap,先要通过glActiveTexture,enable一个纹理单元,然后通过glBindTexture,把这个texture绑定到这个纹理单元上。然后保持这个纹理单元处于active的状态,再调用这个API,来生成指定纹理的mipmap。如果输入了错误的target,就会出现GL_INVALID_ENUM的错误。

这个函数没有输出参数。除了刚才那些因为参数问题导致的错误,还有,如果target是CubeMap texture,但是它的6个面的width、height、format、type并非完全相同,则会出现GL_INVALID_OPERATION的错误。而这种6个面的width、height、format、type并非完全相同的CubeMap texture,我们则称它为cube non-complete。还有上个课时也说过,在OpenGL ES2.0中NPOT的texture是不支持mipmap的,所以如果对NPOT的texture调用这个API生成mipmap,就会出现GL_INVALID_OPERATION的错误。最后还有,如果指定的texture的第0层,是压缩格式的纹理内容,那么,就会出现GL_INVALID_OPERATION的错误。

经过这个API,会根据第0层的数据产生一组mipmap数据,这组mipmap数据会替换掉texture之前的除了第0层之外的所有层的数据,第0层保持不变。所有层数据的internalformat都与第0层保持一致,每层的宽高都是上一层宽高除以2,一直到宽高均为1的一层。然后除了第0层之外,每一层的数据都是通过上一层计算出来的,算法也比较简单,一般都是根据四个像素点的值算出一个像素点的值即可,OpenGL ES并没有规定使用什么算法,不过OpenGL ES会通过glHint这个API,建议使用什么算法,glHint这个API一会我们再说。

void glHint(GLenum target, GLenum mode);

我们知道,OpenGL ES的Spec规定了OpenGL ES API的功能,但是具体如何实现的,则是GPU driver的开发人员根据自己的想法实现的,比如刚才我们说到的glGenerateMipmap的功能就是给指定texture生成mipmap数据,但是具体的生成算法,就是根据开发人员自己来定了。但是开发人员可以设计多种生成算法,然后再通过glHints来选择一种作为生成多级纹理的算法。

这个函数的第一个输入参数用于指定一种GPU行为,在这里只能输入GL_GENERATE_MIPMAP_HINT,否则的话,则会出现GL_INVALID_ENUM的错误。也就是说在OpenGL ES2.0中,只有生成多级纹理的算法,可以通过这个API来选择。第二个输入参数也就是针对刚才指定的GPU的行为,使用哪种方式去实现。这里有三种选择,默认是GL_DONT_CARE,也就是随意选择一种算法。另外两种分别是GL_FASTEST,顾名思义,就是选择一种最有效率的算法。还有GL_NICEST,就是选择一种生成纹理最正确,质量最高的算法。如果输入了其他值,则会出现GL_INVALID_ENUM的错误。虽然这里做出了看似正确的选择,但是哪个算法是最有效率的,而哪个算法是最正确、质量最高的,这种还是需要在GPU中写算法的时候就指定出来,指定好哪个算法是最有效率的,哪个算法是最正确、质量最高的。当然,也可以不指定,然后在GPU driver中选择忽略这个API。

这个函数没有输出参数。

void glDeleteTextures(GLsizei n, const GLuint * textures);

纹理一旦被传输至GPU,就会一直留在GPU管理的内存中。因此我们应该留意那些不再被使用的纹理,及时的从GL内存中删除它们,以减少应用程序内存的占用。所以当texture不再被需要的时候,则可以通过glDeleteTextures这个API把texture object name删除。

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

这个函数没有输出参数。当texture object被删除之后,其中的内容也会被删掉,名字也会被释放,可以被glGenTextures重新使用。如果被删除的texture正在处于bind状态,那么就相当于先将该texture关联的纹理单元active,然后执行了一次glBindTexture把对应的binding变成binging 0,也就相当于什么都没有bind了。

生成纹理的相关OpenGL ES API已经串讲完毕,总结一下这些API的使用流程。

先通过glGenTextures给生成一个新的texture object name。然后选择一个纹理单元,把这个texture object name生成纹理并绑定到这个纹理单元上。所以通过glActiveTexture把纹理单元enable,然后再通过glBindTexture把这个texture与GL_TEXTURE_2D绑定,我们知道这个texture object name是刚创建的,所以在执行这个命令的时候,我们知道,会先生成一个2D的texture object,然后把这个texture object放入刚才enable的那个纹理单元中。下面再通过glTexParameter对纹理设置参数。之后会使用对应的API去生成纹理内容,比如通过glTexImage2D,传入了target GL_TEXTURE_2D,传入了第几层mipmap,传入了准备好的internalformat、宽、高、format、type和data信息。纹理图片经历了解包、归一划、转换为RGB格式、归一划的过程,存储到了GPU中,在内存中,需要按照UNPACK_ALIGNMENT对齐。当CPU的纹理图片unpack到了GPU,开发者可以把CPU端的纹理图片删除。如果数据来源自CPU的JPG、PNG图片,这些格式图片不包含mipmap信息,而如果来自于pvr、etc之类的用于生成压缩纹理的原始图片,才可能会有mipmap信息。如果需要的话,再通过glHint设置生成mipmap的算法,然后通过glGenerateMipmap生成mipmap信息。这样的话一张纹理就生成了。当纹理使用完毕后,就要通过glDeleteTextures这个API把原来对应的纹理删除,并且把texture object name reset为0。

截至到现在,OpenGL ES非常非常重要的一个模块,纹理,我们已经将基础部分讲完了。首先,先介绍了如何从原始图片中获取到我们生成纹理所需要的信息,然后介绍了如何根据获取的信息,通过一系列的OpenGL ES API生成纹理,并设置纹理的属性,介绍了纹理映射到绘制buffer上的机制和原理。由于纹理是应用程序中,最影响包体大小,以及最占用内存大小的,所以下节课,我们将讲述如何对纹理进行优化。

本节教程就到此结束,希望大家继续阅读我之后的教程。

谢谢大家,再见!


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