OpenGL ES

一、OpenGL ES介绍

OpenGL(Open Graphics Library)定义了一个跨编程语言、跨平台编程的专业图形程序接口。可用于二维或三维图像的处理和渲染,它是一个功能强大、调用方便的底层图形库。对于嵌入式设备,其提供了OpenGL ES(OpenGL for Embeddled Systems)版本,该版本是针对手机、Pad等嵌入式设备而设计的,是OpenGL的一个子集。到目前为止,OpenGL已经经历过很多版本的迭代与更新,最新版本是3.0,而使用最广泛的还是OpenGL ES 2.0版本。本文是基于2.0版本进行编程并实现图像的处理与渲染,并且只讨论2D部分的内容。

由于OpenGL是基于跨平台设计的,所以在每个平台上都要有它的具体实现,即要提供OpenGL ES的上下文环境以及窗口的管理。在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理将交由各个设备自己来完成,上下文环境也是一样的,其在各个平台上都有自己的实现。在iOS平台上使用EAGL提供本地平台对OpenGL ES的实现。

这里需要介绍一个库——libSDL,它可以为开发者提供面向libSDL的API编程,libSDL内部会解决多个平台的OpenGL上下文环境以及窗口的管理问题,开发者只需要交叉编译这个库到各自的平台上就可以做到只写一份代码即可运行多个平台。其中FFmpeg中的ffplay这一工具就是基于libSDL进行开发的。但是对于移动开发者来讲,这样就会失去一些更加灵活的控制,甚至某些场景下的功能不能实现。

上面介绍了OpenGL ES是什么,下面再来介绍一下OpenGL ES能做什么。其实从名字上就可以看出来,OpenGL主要是做图形图像处理的库,尤其是在移动设备上进行图形图像处理,它的性能优势更能体现出来。GLSL(OpenGL Shading Language)是OpenGL的着色器语言,开发人员利用这种语言编写程序运行在GPU(Graphic Processor Unit,图形图像处理单元,可以理解为是一种高并发的运算器)上以进行图像的处理和渲染。GLSL着色器代码分为两个部分,即Vertex Shader(顶点着色器)与Fragment Shade(片元着色器)两部分,分别完成各自在OpenGL渲染管线中的功能。对于OpenGL ES,业界有一个著名的开源库GPUImage,它的实现非常优雅,尤其是在iOS平台上实现的非常完备,不仅有摄像头采集实时渲染、视频播放器、离线保存等功能,更有强大的滤镜实现。在GPUImage的滤镜实现中,可以找到大部分图形图像处理Shader的实现,包括:亮度、对比度、饱和度、色调曲线、白平衡、灰度等调整颜色的处理,以及锐化、高斯模糊等图像像素处理的实现等,还有素描、卡通效果、浮雕效果等视觉效果的实现,最后还有各种混合模式的实现等。当然除了GPUImage提供的这些图像处理的Shader之外,开发者也可以自己实现一些有意思的Shader,比如美颜滤镜效果、瘦脸效果以及粒子效果等。

二、OpenGL ES的实践

1.OpenGL 渲染管线

要想学习着色器,并理解着色器的工作机制,就要对OpenGL 固定的渲染管线有深入的了解。同样,先来统一一下术语。

  • 几何图元:包括点、直线、三角形,均是通过顶点(vertex)来指定的。
  • 模型:根据几何图元创建的物体。
  • 渲染:计算机根据模型创建图像的过程。

最终渲染过程结束之后,人眼所能看到的图像就是由屏幕上的所有像素点组成的,在内存中,这些像素点可以组织成一个大的一维数组,每4个字节即表示一个像素点的RGBA数据,而在显卡中,这些像素点可以组织成帧缓冲区的形式,帧缓冲区保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。理解了帧缓冲区的概念,接下来就来讨论一下OpenGL的渲染管线,这部分内容对于OpenGL来说是非常重要的。

那么OpenGL的渲染管线具体是做什么的呢?其实就是OpenGL引擎渲染图像的流程,也就是说OpenGL引擎是一步一步地将图片渲染到屏幕上去的过程。渲染管线分为以下几个阶段。

阶段一:指定几何对象

所谓几何对象,就是上面说过的几何图元,这里将根据具体执行的指令绘制几何图元。比如,OpenGL提供给开发者的绘制方法glDrawArrays,这个方法里的第一个参数是mode,就是制定绘制方式,可选值有一下几种。

  • GL_POINT:以点的形式进行绘制,通常用在绘制粒子效果的场景中。
  • GL_LINES:以线的形式进行绘制,通常用在绘制直线的场景中。
  • GL_TRIANGLE_STRIP:以三角形的形式进行绘制,所有二维图像的渲染都会使用这种方式。

具体选用哪一种绘制方式决定了OpenGL渲染管线的第一阶段应如何去绘制几何图元,所以这就是第一阶段指定的几何对象。

阶段二:顶点处理

不论以上的几何对象是如何指定的,所有的几何数据都将会经过这个阶段。这个阶段所做的操作就是,根据模型视图和投影矩阵进行变换来改变顶点的位置,根据纹理坐标与纹理矩阵来改变纹理坐标的位置,如果涉及三维的渲染,那么这里还要处理光照计算与法线变换。这里的输出是以gl_Position来表示具体的顶点位置的,如果是以点来绘制几何图元,那么还应该输出gl_PointSize。

阶段三:图元组装

在经过阶段二的顶点处理操作之后,还是纹理坐标都是已经确定好了的。在这个阶段,顶点将会根据应用程序送往图元的规则(如GL_POINT、GL_TRIANGLE_STRIP),将纹理组装成图元。

阶段四:栅格化操作

由阶段三传递过来的图元数据,在此将会分解成更小的单元并对应于帧缓冲区的各个像素。这些单元称为片元,一个片元可能包含窗口颜色、纹理坐标等属性。片元的属性是根据顶点坐标利用插值来确定的,这就是栅格化操作,也就是确认好每一个片元是什么。

阶段五:片元处理

通过纹理坐标取得纹理(texture)中相对应的片元像素值(texel),根据自己的业务处理(比如提亮、饱和度调节、对比度调节、高斯模糊)来变换这个片元的颜色。这里的输出是gl_FragColor,用于表示修改之后的像素的最终结果。

阶段六:帧缓冲操作

该阶段主要执行帧缓冲的写入操作,这也是渲染管线的最后一步,负责将最终的像素值写入到帧缓冲区中。

前面也提到过,OpenGL ES提供了可编程的着色器来代替渲染管线的某个阶段。具体如下所示:
Vertex Shader(顶点着色器)用来替代顶点处理阶段。
Fragment Shader(片元着色器,又称为像素着色器)用来替换片元处理阶段。

glFinish和glFlush

提交给OpenGL的绘图指令并不会马上发送给图形硬件执行,而是放到一个缓冲区里面,等待缓冲区满了之后再将这些指令发送给图形硬件执行,所以指令较少或较简单时是无法填充缓冲区的,这些指令自然不能马上执行以达到所需要的效果。因此每次写完绘图代码,需要让其立即完成效果时,开发者需要在代码后面添加glFlush()或glFinish()函数。

  • glFlush()的作用是将缓冲区中的指令(无论是否为满)立即发送给图形硬件执行,发送完立即返回。
  • glFinish()的作用也是将缓冲区中的指令(无论是否为满)立即发送给图形硬件执行,但是要等待图形硬件执行完成之后才返回这些指令。

2.GLSL语法与内建函数

GLSL是为了实现着色器的功能而向开发人员提供的一种开发语言。

(1)GLSL的修饰符与基本数据类型

具体来说,GLSL的语法与C语言非常类似,学习一门语言,首先要看它的数据类型表示,然后再学习具体的运行流程。对于GLSL,其数据类型表示具体如下.

修饰符

具体如下:

  • const:用于声明非可写的编译时常量变量。
  • attribute:用于经常更改的信息,只能在顶点着色器中使用。
  • uniform:用于不经常更改的信息,可用于顶点着色器和片元着色器。
  • varying:用于修饰从顶点着色器向片元着色器传递的变量
基本数据类型

int、float、bool,这些与C语言都是一致的,需要强调的一点就是,这里面的float是有一个修饰符的,即可以指定精度。三种修饰符的范围(范围一般视显卡而定)和应用情况具体如下。

  • highp:32bit,一般用于顶点坐标(vertex Coordinate)。
  • medium:16bit,一般用于纹理坐标(texure Coordinate)。
  • lowp:8bit,一般用于颜色显示(color)。
向量类型

向量类型是Shader中非常重要的一个数据类型,因为在做数据传递的时候需要经常传递多个参数,相较于写多个基本数据类型,使用向量类型是非常好的选择。列举一个最经典的例子,要将物体坐标和纹理坐标传递到Vertex Shader中,用的就是向量类型,每一个顶点就是一个四维向量,在Vertex Shader中利用这两个四维向量即可完成自己的纹理坐标映射操作。声明方式如下(GLSL代码):

attribute vec4 position;
矩阵类型

有一些效果器需要开发者传入矩阵类型的数据,比如后面会接触到的怀旧效果器,就需要传入一个矩阵来改变原始的像素数据。声明方式如下:

uniform lowp mat4 colorMatrix;

上面的代码表示了一个4x4的浮点矩阵,如果是mat2就是2x2的浮点矩阵,如果是mat3就是3x3的浮点矩阵。若要传递一个矩阵到实际的Shade中,则可以直接调用如下函数:

glUniformMarix4fv(mColorMatrixLocation,1,false,mColorMatrix);
纹理类型

一般仅在Fragment Shader中使用这个类型,二维纹理的声明方式如下

uniform sample2D texSampler;

当客户端接收到这个句柄时,就可以为它绑定一个纹理,代码如下:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texId);
glUniformli(mGLUniformTexture,0);

上述代码第一行激活的是哪一个纹理句柄,第三行代码中的第二个参数需要传递对应的Index,就像代码中激活的纹理句柄是GL_TEXTURE0,对应的Index就是0,如果激活的纹理句柄是GL_TEXTURE1,那么对应的Index就是1,在不同的平台上句柄的个数也是不一样,但是一般都会在32个以上。

varying

这个修饰符变量用于在Vertex Shader和Fragment Shader之间传递函数。首先在顶点着色器中声明这个类型的变量代表纹理的坐标点,并且对这个变量进行赋值,代码如下:

attribute vec2 texoord;
varying vec2 v_texcoord;
void main(void)
{
      // 计算顶点坐标
      v_texcoord = texcoord;
}

紧接着在Fragment Shader中也声明同名的变量,然后使用texture2D方法取出二维纹理中该纹理坐标点上的纹理像素值,代码如下

varying vec2 v_texcoord;
vec4 texel = texture2D(texSampler,v_texcoord);

取出了该坐标点上的像素值之后,就可以进行像素变化操作了,比如说提高对比度,最终将改变的像素值复制给gl_FragColor。

(2)GLSL的内置函数与内置变量

首先来看内置变量,最常见的是两个Shader的输出变量。
先来看Vertex Shader的内置变量:

vec4 gl_position;

上述代码用来设置顶点转换到屏幕坐标的位置,Vertex Shader一定要去更新这个数值。另外还有一个内置变量,

float gl_pointSize;

在粒子效果的场景下,需要为粒子设置大小,改变该内置变量的值就是为了设置每一个粒子矩形的大小。
其次是Fragment Shader的内置变量,代码如下

vec4 gl_FragColor;

上述代码用于指定当前纹理坐标所代表的像素点的最终颜色值。

然后是内置函数,具体的函数可以去官方文档中查询,这里仅介绍几个常用的函数。

  • abs(genType x):绝对值函数。

  • floor(genType x):向下取整函数。

  • ceil(genType x):向上取整函数。

  • mod(genType x,genType y):取模函数。

  • min(genType x,genType y):取的最小值函数。

  • max(genType x,genType y):取得最大值函数。

  • clamp(genType x,genType y,genType z):取得中间值函数。

  • step(genType x,genType y):如果x<edge,则返回0.0,否则返回1.0。

  • smoothstep(genType edge0,genType edge1,genType x):如果x<=edge0,则返回0.0;如果x>=edge1,则返回1.0;如果edge0<x<edge1,则执行0-1的平衡插值。

  • mix(genType x,genType y,genType a):返回线性混合的x和y,用公式表示为x(1-a)+ya,这个函数在mix两个纹理图像的时候非常有用。

GLSL的控制流与C语言非常类似,既可以使用for、while、以及do-while实现循环,也可以使用if和if-else进行条件分支的操作。

3.创建显卡执行程序

如何将Shader传递给OpenGL的渲染管线。

1)创建shader的过程

第一步是调用glCreateShader方法创建一个对象,作为shade的容器,该函数会返回一个容器的句柄,函数的原型如下:

GLuint glCreateShader(GLenum shaderType);

函数原型中的参数shaderType有两种类型,当要创建VertexShader时,开发者应该传入类型GL_VERTEX_SHADER;当要创建FragmentShader时,开发者应该传入GL_FRAGMENT_SHADER类型。

下一步就是为创建的这个shader添加源代码,源代码就是根据GLSL语法和内嵌函数编写的两个着色器程序(Shader),其为字符串类型。函数原型如下:

void glShaderSource(GLuint shader,int numOfStrings,const char **strings,int *lenOfStrings)

上述函数的作用就是把开发者编写的着色器程序加载到着色器句柄所关联的内存中。

最后一步就是编译该Shader,编译Shader的函数原型如下:

void glCompileShader(GLuint shader);

待编译完成之后,还需要验证该Shader是否编译成功了。那么,应该如何验证呢?使用下面的函数即可进行验证:

void glCetShaderiv(GLuint shader,GLenum pname,GLint *params);

其中第一个参数就是需要验证的Shader句柄;第二个参数值是需要验证的Shader状态值,这里一般是验证编译是否成功,该状态值一般是选取GL_COMPILE_STATUS;第三个参数是返回值。当返回1时,则说明该Shader是编译成功的;如果为0,则说明该Shader没有被编译成功,此时获取的是改Shader的另外一个状态,该状态值应该选取GL_INFO_LOG_LENGTH,返回值返回的则是错误原因字符串的长度,我们可以利用这个长度分配出一个buffer,然后调用获取Shader的InfoLog函数,函数原型如下:

void glGetShaderInfoLog(GLuint object,int maxLen,int *len,char *log);

之后可以把InfoLog打印出来,以帮助我们调试实际Shader中的错误。

2)如何通过两个Shader来创建显卡可执行程序

首先创建一个对象,作为程序的容器,此函数将返回容器的句柄。函数原型如下:

GLuint glCreateProgram(void);

第二步是把前文编译的Shader附加到刚刚创建的程序中,调用的函数名称如下:

void glAttachShader(GLuint program,GLuint shader);

第一个参数就是传入上一步返回的程序容器的句柄,第二个参数就是编译的Shader容器的句柄,当然要为每一个Shader都调用一次这个方法才能把两个Shader都关联到Program中去。
最后一步就是链接程序了,链接函数原型如下:

void glLinkProgram(GLuint program);

传入参数就是程序容器的句柄,那么这个程序有没有链接成功呢?OpenGL提供了一个函数来检查该程序的状态,函数原型如下:

void glGetProgramiv(GLuint program,GLenum pname,GLint *params);

第一个参数就是传入程序容器的句柄,第二个参数代表需要检查该程序的哪一个状态,这里传入的是GL_LINK_STATUS,最后一个参数就是返回值。返回值为1则代表链接成功,如果返回值为0则代表链接失败。如果想获取具体的错误信息,第二个参数要传递GL_INFO_LOG_LENGTH,代表获取该程序的InfoLog的长度,获取到长度之后我们分配出一个char *的内存空间以获取InfoLog,函数原型如下:

void glGetProgramInfoLog(GLuint object,int maxLen,int *len,char *log);

该函数返回InfoLog之后可以将其打印出来。

使用构建的这个函数,调用glUseProgram方法就可以了。如果想让其完全运行在手机上,还需要为其提供一个上下文环境。

三、iOS上下文环境搭建

在iOS平台上不允许开发者使用OpenGL ES直接渲染屏幕,必须使用FrameBuffer与RenderBuffer来进行渲染。若要使用EAGL,则必须先创建一个RenderBuffer,然后让OpenGL ES渲染到该RenderBuffer上去。而该RenderBuffer则需要绑定到一个CAEAGLLayer上面去,这样开发者最后调用EAGLContext的presentRenderBuffer方法,就可以将渲染结果输出到屏幕上去了。实际上,在调用这个方法时,EAGL也会执行类似于前面的swapBuffer过程,将OpenGL ES渲染的结果绘制到物理屏幕上去(View的Layer),具体使用步骤如下。
首先编写一个View,继承自UIView,然后重写父类UIView的一个方法layerClass,并且返回CAEAGLLayer类型:

+ (Class)layerClass
{
      return  [CAEAGLLayer class];
}

然后在该View的initWithFrame方法中,获得layer并且强制类型转换为CAEAGLLayer类型的变量,同时为layer设置参数,其中包括色彩模式等属性:

- (id)initWithFrame:(CGRect)frame{
      if(self = [super initWithFrame:frame]){
              CAEAGLLayer *eaglLayer = (CAEAGLLayer *)[self layer];
 /*
     kEAGLDrawablePropertyRetainedBacking 设置是否需要保留已经绘制到图层上面的内容 用NSNumber来包装,kEAGLDrawablePropertyRetainedBacking 
     为FALSE,表示不想保持呈现的内容,因此在下一次呈现时,应用程序必须完全重绘一次。将该设置为 TRUE 对性能和资源影像较大,
     因此只有当renderbuffer需要保持其内容不变时,我们才设置 kEAGLDrawablePropertyRetainedBacking  为 TRUE。
     kEAGLDrawablePropertyColorFormat 设置绘制对象内部的颜色缓冲区的格式 32位的RGBA的形式
     包含的格式
     kEAGLColorFormatRGBA8; 32位RGBA的颜色 4x8=32
     kEAGLColorFormatRGB565; 16位的RGB的颜色
     kEAGLColorFormatSRGBA8 SRGB
*/
              NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO],kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGB565,kEAGLDrawablePropertyColorFormat,nil];
              [eaglLayer setOpaque:YES];
              [eaglLayer setDrawableProperties:dict];
       }
       return self;
}

接下来构造EAGLContext与RenderBuffer并且绑定到Layer上,之前也提到过,必须为某一个线程绑定绑定OpenGL ES上下文。所以首先必须开辟一个线程,开发者在iOS中开辟一个新线程有多种方式,可以使用dispatch_queue,也可以使用NSOperationQueue,甚至使用pthread也可以,反正必须在一个线程中执行以下操作,首先创建OpenGL ES的上下文:

EAGLContext *_context;
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

然后实施绑定操作,代码如下:

[EAGLContext setCurrentContext:_context];

此时就已经为该线程绑定了刚刚创建好的上下文环境了,也就是已经建立好了EAGL与OpenGL ES的连接,接下来在建立另一端的连接。
创建帧缓冲区:

glGenFramebuffers(1,&_FrameBuffer);

创建绘制缓冲区:

glGenRenderbuffers(1,&renderbuffer)

绑定帧缓冲区到渲染管线:

glBindFramebuffer(GL_FRAMEBUFFER,_FrameBuffer);

绑定绘制缓冲区到渲染管线:

glBindRenderbuffer(GL_RENDERBUFFER,_renderbuffer);

为绘制缓冲区分配存储区,此处将CAEAGLLayer的绘制存储区作为绘制缓冲区的存储区:

[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];

获取绘制缓冲区的像素高度:

glGetRenderBufferParameteriv(GL_RENDER_BUFFER,GL_RENDER_HEIGHT,&_backingHeight);

获取绘制缓冲区的像素高度:

glGetRenderBufferParameteriv(GL_RENDER_BUFFER,GL_RENDER_WIDTH,&_backingWidth);

将绘制缓冲区绑定到帧缓冲区:

// 把GL_RENDERBUFFER里的colorRenderbuffer附在GL_FRAMEBUFFER的GL_COLOR_ATTACHMENT0(颜色附着点0)上
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_RENDERBUFFER,_renderbuffer);

检查FrameBuffer的status:

GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(status != GL_FRAMEBUFFER_COMPLETE){
       // failed to make complete frame buffer object
}

至此我们就将EAGL与Layer(设备的屏幕)连接起来了,绘制完一帧之后(当然绘制过程也必须在这个线程之中),调用一下代码:

[_context presentRenderbuffer:GL_RENDERBUFFER];

这样就可以将绘制的结果显示到屏幕上了。

四、OpenGL ES中的纹理

OpenGL中的纹理可以用来表示图像、照片、视频画面等数据,在视频渲染中只需要处理二维的纹理,每个二维纹理都由很多小的纹理元素组成,它们都是小块数据,类似于前面章节所说的像素点。要使用纹理,最常用的是直接从一个图像文件加载数据。
为了访问到每一个纹理元素,每个二维纹理都有自己的坐标空间,其范围是从左下角的(0,0)到右上角的(1,1)。


20181218202105492.png

我们所熟知的不论是计算机还是手机的屏幕坐标系,x轴从左到右都是从0到1,y轴从上到下是从0到1,与图片的存储恰好是一致的,假设图片的存储是把所有的像素点都存储到一个大数组中,图片存储的第一个像素点是左上角的像素点(即第一排第一列的像素点),然后第二个像素点(第一排第二列)存储在数组的第二个元素中,那么,这里的坐标和OpenGL中的纹理坐标正好做了一个180度的旋转。

下面再来看一下如何加载一张图片作为OpenGL中的纹理,首先要在显卡中创建一个纹理对象,OpenGL ES提供的方法原型如下:

void glGenTextures(GLsizei n,GLuint *textures)

这个方法传递进去的第一个参数是需要创建几个纹理对象,并且把创建好的纹理对象的句柄放到第二个参数中去,所以第二个参数是一个数组(指针)的形式。如果只需要创建一个纹理对象的话,则只需要声明一个GLuint类型的texId,然后针对该纹理ID取地址,并将其作为第二个参数,就可以创建出这个纹理对象了,代码如下:

glGenTextures(1,&texId);

执行完这行代码之后,就会在显卡中创建一个纹理对象,并且把该纹理对象的返回给texId变量。紧接着开发者要操作该纹理对象,但是在OpenGL ES的操作过程中必须告诉OpenGL ES具体操作的是哪一个纹理对象,所以必须调用OpenGL ES提供的一个绑定纹理对象的方法,调用代码如下:

glBindTexture(GL_TEXTURE_2D,texId);

执行完上面这行代码之后,下面的操作就都是针对于texId这个纹理对象的了,最终对该纹理的对象操作完毕之后,我们可以调用一次解绑定的代码:

glBindTexture(GL_TEXTURE_2D,0);

这行代码执行完毕之后,代表开发者不会对texId纹理做任何操作了,所以上面这行代码只在最后的时候才调用。

接下来就是最关键的部分,即如何将本地磁盘中的一个PNG的图片上传到显卡中的这个纹理对象上。在将图片上传到这个纹理上之前,首先应该要对这个纹理对象设置一些参数,具体参数有哪些?其实就是纹理的过滤方式,当纹理对象(可以理解为一张图片)被渲染到物体表面上的时候(实际上OpenGL绘制管线将纹理的元素映射到OpenGL生成的片段上的时候),有可能要被放大或者缩小,而当其放大或者缩小的时候,具体应该如何确定每个像素是如何被填充的,就由开发者配置的纹理对象的纹理过滤器来指明。
magnification(放大):

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

minification(缩小)

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

一般在视频的渲染与处理的时候使用GL_LINEAR这种过滤方式,该过滤方式称为双线性过滤,可使用双线性插值平滑像素之间的过渡,OpenGL会使用四个临近的纹理元素,并在它们之间用一个线性插值算法做插值,该过滤方式是最主要的过滤方式,当然OpenGL中还提供了另外几种过滤方式。常见的有GL_NEAREST,称为最临近过滤,该方式将为每个片段选择最近的纹理元素,但是当其放大的时候会有很严重的锯齿效果(因为相当于将原始的直接放大,其实就是降采样),而当其缩小的时候,因为没有足够的片段来绘制所有的纹理单元,(这个是真正的降采样),许多细节都会丢失;其实OpenGL还提供了另外一种技术,称为MIP贴图,但是这种技术会占用更多的内存,其优点是渲染也会更快。当缩小和放大到一定程度之后效果也比双线性过滤的方式更好,但是其对纹理的尺寸及内存的占用是有一定限制的,不过,在视频的处理以及渲染的时候不需要放大或者缩小这么多倍,所以在进行视频的处理以及渲染的场景下,MIP贴图并不适用。

紧接着来看一下对于纹理对象的另外一个设置,也就是在纹理坐标系的s轴和t轴的纹理映射过程中用到的重复映射或者简约映射的规则,代码如下:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP_TO_EDGE);

上述代码所表示的含义是,将该纹理的s轴和t轴的坐标设置为GL_CLAMP_TO_EDGE类型,因为纹理坐标可以超出(0,1)的范围,而按照上述设置规则,所有大于1的纹理都要设置为1,所有小于0的纹理都要置为0。

接下来,就是将PNG素材的内容放到该纹理对象上,OpenGL的大部分纹理一般都只接受RGBA类型的数据(否则还得去转化),所以我们需要对PNG这种压缩格式进行解码操作,如果想要采用一种更通用的方式,那么可以引用libpng库来进行解码操作,当然也可以使用各自平台的API进行解码,最终可以得到RGBA数据。待得到RGBA数据之后,记为uint8_t数组类型的pixels,然后执行如下操作:

glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,pixels);

这样就可以将该RGBA的数组表示的像素内容上传到显卡里面texId所代表的纹理对象中去了,以后只要使用该纹理对象,其实表示的就是这个PNG图片。

OpenGL中的纹理表示如何为物体增加细节,现在我们已经准备好了该纹理,那么如何把这张图片(或者说这个纹理)绘制到屏幕上呢?首先来看一下OpenGL中的物体坐标系,物体坐标系中x轴从左到右是从-1到1变化的,y轴从下到上是从-1到1变化的,物体的中心点恰好是(0,0)的位置。

接下来的任务就是如何将这个纹理绘制到屏幕上,其实相关的基础知识已经都讲解过了,首先是搭建好各自平台的OpenGL ES的环境(包括上下文与窗口管理),然后创建显卡可执行程序,书写Vertex Shader,代码如下:

static char * COMMON_VERTEX_SHADER = 
"attribute vec4 position;              \n" 
"attribute vec2 texcoord;             \n" 
"varying vec2 v_texcoord;            \n" 
"                                                       \n" 
"void main(void)                            \n" 
"{                                                      \n" 
"     gl_Position = position;             \n" 
"     v_texcoord = texcoord;            \n" 
"}                                                       \n" ;

在客户端代码中,开发者要从VertexShader中读取出两个attribute,并放置到全局变量的mGLVertexCoords与mGLTextureCoords中,接下来是Fragment Shader的内容,代码如下所示:

static char * COMMON_FRAG_SHADER = 
"precision highp float;                                   \n"
"varying highp vec2 v_texcoord;                   \n"
"uniform sampler2D texSampler;                  \n"
"void main()  {                                                  \n"
"     gl_FragColor = texture2D(texSample,v_texcoord);                                           \n"

从FragmentShader中读取出来的uniform会放置到mGLUniformTexture变量里,利用上面两个Shader创建好的Program,称为mGLProgId.紧接着进行真正的绘制操作,下面将详细讲一下绘制部分。
1)规定窗口的大小:

glViewport(0,0,screenWidth,screenHeight);

假定screenWidth表示绘制区域的宽度,screenHeight表示绘制区域的高度。
2)使用显卡绘制程序

glUserProgram(mGLProgId);

3)设置物体坐标:

GLfloat vertices[] = {-1.0f,-1.0f,1.0f,-1.0f,-1.0f,,1.0f,1.0f,1.0f};
glVertexAttribPointer(mGLVertexCoords,2,GL_FLOAT,0,0,vertices);
glEnableVertexAttribArray(mGLVertexCoords);

4)设置纹理坐标

GLfloat texCoords1[] = {0.0f,0.0f,1.0f,0.0f,0.0f,,1.0f,1.0f,1.0f};
GLfloat texCoords2[] = {0.0f,1.0f,1.0f,1.0f,0.0f,,0.0f,1.0f,0.0f};
glVertexAttribPointer(mGLVertexCoords,2,GL_FLOAT,0,0,texCoords2);
glEnableVertexAttribArray(mGLVertexCoords);

这里需要注意的是texCoords2这个纹理坐标,因为其纹理对象是将一个PNG图片的RGBA格式的形式上传到显卡上(即计算机坐标),如果该纹理对象是OpenGL中的一个普通纹理对象,则需要使用texCoords1,这两个纹理坐标恰恰就是要做一个上下的翻转,从而将计算机坐标系和OpenGL坐标系进行转换。
5)指定将要绘制的纹理对象并且传递给对应的FragmentShader:

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texId);
glUniformli(mGLVertexCoords,0);

6)执行绘制操作:

glDrawArrays(GL_TRIANGLE_STRIP,0,4);

至此就可以在绘制区域(屏幕)绘制出最初的PNG图片了。
如果该纹理对象不再使用了,则需要将其删除掉,需要执行的代码是:

glDeleteTextures(1,&texId);

当然,只有在最终不再使用这个纹理的时候才会调用上述的这个方法,如果不调用,会造成显存的泄漏。

推荐阅读更多精彩内容