OpenGL ES入门及GLSL

使用OpenGL ES最关心的问题

1、如何在iOS上搭建OpenGL ES环境
2、如何链接GLSL
3、如何通过GLSL输入数据

参考:
《音视频开发进阶指南》、网络资料

GLSL介绍

GLSL(OpngGL Shading Language)是OpenGL的着色器语言,开发人员利用这种语言编写程序运行在GPU(Graphic Processor Unit,图形图像处理单元,可以理解为是一种高并发的运算器)上以进行图像的处理或渲染。

1.OpenGL渲染管线

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

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

最终渲染结束之后,就是人眼看到的图像。

图像是由屏幕上的所有像素点组成的。

  • 在内存中:这些像素点可以组织成一个大的一维数组,每4个Byte即表示一个像素点的RGBA数据。
  • 在显卡中:这些像素点可以组织成帧缓冲区(FrameBuffer)的形式,帧缓冲区保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。

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

阶段一:指定几何对象

所谓几何对象,就是上面说过的几何图元,这里将根据具体执行的指令绘制几何图元
比如,OpenGL提供给开发者的绘制方法:

 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 

这个方法里面的第一个参数是mode,就是制定绘制方式,可选值有以下几种。

  • GL_POINTS:以点的形式进行绘制,通常用在绘制粒子效果的场景中。
  • GL_LINES:以线的形式进行绘制,通常用在绘制直线的场景中。
  • GL_TRIANGLE_STRIP:以三角形的形式进行绘制,所有二维图像的渲染都会使用这种方式。
    可参考链接:https://blog.csdn.net/csxiaoshui/article/details/54923763
    image.png

具体选用哪一种绘制方式决定了OpenGL渲染管线的第一阶段应如何去绘制几何图元

阶段二:顶点处理

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

 attribute vec4 position;
attribute vec2 textCoordinate;
uniform mat4 rotateMatrix;

varying lowp vec2 varyTextCoord;

void main()
{
    varyTextCoord = textCoordinate;
    
    vec4 vPos = position;

    vPos = vPos * rotateMatrix;

    gl_Position = vPos;
} 

阶段三:图元组装

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

阶段四:栅(shan)格化操作

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

注:光栅化(Rasterize/rasteriztion)。这个词儿Adobe官方翻译成栅格化或者像素化。没错,就是把矢量图形转化成像素点儿的过程。我们屏幕上显示的画面都是由像素组成,而三维物体都是点线面构成的。要让点线面,变成能在屏幕上显示的像素,就需要Rasterize这个过程。就是从矢量的点线面的描述,变成像素的描述。如下图,这是一个放大了1200%的屏幕,前面是告诉计算机我有一个圆形,后面就是计算机把圆形转换成可以显示的像素点。这个过程就是Rasterize。
链接:https://www.zhihu.com/question/29163054/answer/46695506

注:简单来讲,图元就是组成图像的基本单元,比如三维模型中的点、线、面等等,注意图元(entity)与片元(primitive)的区别,片元就是以后的像素点,它比像素多一些位置、法向量等属性。逐个片元操作有像素所有权操作(确定目标像素可见还是被一个重叠的窗口盖住了),剪切测试、Alpha测试、模板测试、混合等。而片段(fragments)是指具有相同属性的一小部分像素区域。
链接:https://baike.baidu.com/item/图元/2303188?fr=aladdin

可参考链接:https://www.cnblogs.com/yangai/p/6764383.html

image.png

阶段五:片元处理

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

 varying lowp vec2 varyTextCoord;

uniform sampler2D colorMap;


void main()
{
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}
 

阶段六:帧缓冲操作

该阶段主要执行帧缓冲的写入操作,这也是渲染管线的最后一步,负责将最终的像素值写到帧缓冲区(像素在显卡中的保存形式/位置)中。

着色器代替了渲染管线中的阶段二:顶点处理阶段五:片元处理

2.GLSL语法与内建函数

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

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

GLSL的语法与C语言非常类似,其数据类型表示具体如下。

修饰符:

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

基本数据类型:
int、float、bool,这些与C语言都是一致的,需要强调的一点就是,这里面的float是有一个修饰符的,即可以指定精度。

 precision highp float; 

三种修饰符的范围(范围一般视显卡而定)和应用情况具体如下。

  • high:32bit,一般用于顶点坐标(vertex Coordinate)。
  • medium:16bit,一般用于纹理坐标(texture Coordinate)。
  • lowp:8bit,一般用于颜色表示(color)。
varying   highp vec2 vv2_Texcoord;
mediump vec3 yuv;
lowp    vec3 rgb; 

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

 attribute vec4 position;//物体坐标
attribute vec2 textCoordinate; //纹理坐标

矩阵类型:
矩阵类型在Shader的语法中也是一个非常重要的类型,有一些效果器需要开发者传入矩阵类型的数据,比如怀旧效果器,就需要传入一个矩阵来改变原始的像素数据。声明方式如下(GLSL代码):

uniform lowp mat4 colorMatrix;
uniform mat4 rotateMatrix;

上面的代码表示来一个4x4的浮点矩阵。若要传递一个矩阵到实际的Shader中,则可以直接调用如下函数(客户端代码):

glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]); 

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

uniform sampler2D texSampler;

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

 glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texId);
glUniform1i(mGLUniformTexture, 0);//定义哪个uniform采样器对应哪个纹理单元

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

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

attribute vec2 texcoord;
varying vec2 v_texcoord;

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

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

 varying highp vec2 v_texcoord;
 uniform sampler2D texSampler;
 
 void main()
 {
     vec4 texel = texture2D(texSampler, v_texcoord);
     gl_FragColor = texel;
 }

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

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

内置变量:

最常见的Vertex Shader和Fragment Shader的输出变量。

Vertex Shader的内置变量:
先来看Vertex Shader的内置变量(GLSL代码):

 vec4 gl_Position;

上述代码用来设置顶点转换到屏幕坐标的位置。
另外还有一个内置变量,代码如下(GLSL代码):

float gl_PointSize 

在粒子效果的场景下,需要为粒子设置大小,改变该内置变量的值就是为了设置每一个粒子矩阵的大小。

Fragment Shader的内置变量:
其次是Fragment Shader的内置变量,代码如下(GLSL代码):

  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 edge, genType x):如果x < edge,则返回0.0,否则返回1.0。
  • smooth step(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两个纹理图像的时候非常有用。

其他的角度函数、指数函数、几何函数在这里就不在赘(zhui)述了,大家可以去官方文档进行查询。
对于一个语言的语法来说,剩下的就是控制流部分了,而GLSL的控制流与C语言非常类似,既可以使用for、while以及do-while实现循环,也可以使用if和if-else进行条件分支的操作。

如何链接GLSL

创建显卡执行程序

前面已经学习了GLSL的语法以及内嵌函数,并且也已经完成了一组Shader的实例,那么如何让显卡来运行这一组shader呢?或者说如何用Shader来替换掉OpenGL渲染管线中的那两个阶段(顶点处理与片元处理)呢?下面来学习一下如何将Shader传递给OpenGL的渲染管线。

1.创建Shader的过程

第一步:glCreateShader

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

 GLuint glCreateShader(GLenum shaderType); 

实例:

 GLuint shader = glCreateShader(type); 

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

第二步:glShaderSource

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

  void glShaderSource(GLuint shader, int numOfStrings, const **strings, int *lenOfString);

实例:

 glShaderSource(shader, 1, &sources, NULL); 

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

第三步:glCompileShader

编译该Shader,编译Shader的函数原型如下:

  void glCompileShader(GLuint shader);

实例:

  glCompileShader(shader);

第四步:glGetShaderiv

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

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

实例:

glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE) {
    glDeleteShader(shader);
    NSLog(@"Failed to compile shader:\n");
    return 0;
}

glGetShaderiv的第二个参数一般选取GL_COMPILE_STATUS。
如果没有编译成功,那么开发者肯定需要知道到底是着色器代码中的哪一行出了问题,所以还需要调用上面的函数glGetShaderiv,第二个参数选取为GL_INFO_LOG_LENGTH,此时返回值返回的是错误原因字符串的长度,我们可以利用这个长度分配出一个buffer,然后调用获取Shader的InfoLog函数,函数原型如下:

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

实例:

#ifdef DEBUG
    GLint logLength;
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0)
    {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetShaderInfoLog(shader, logLength, &logLength, log);
        NSLog(@"Shader compile log:\n%s", log);
        free(log);
    }
#endif
    
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    if (status == GL_FALSE) {
        glDeleteShader(shader);
        NSLog(@"Failed to compile shader:\n");
        return 0;
    }

之后可以把InfoLog打印出来,以帮助我们调试实际Shader中的错误。
通过上面的步骤可以创建出Vertex Shader和Fragment Shader,那么,如何通过这两个Shader来创建Program(显卡可执行程序)。

2.创建Program的过程

第一步:glCreateProgram

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

 Glint glCreateProgram(void); 

第二步:glAttachShader

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

 void glAttachShader(GLuint program, Glint shader); 

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

第三步:glLinkProgram

链接程序,函数原型如下:

 void glLinkProgram(GLuint program); 

第四步:glGetProgramiv

检查链接是否成功,函数原型如下:

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

第二个参数传入GL_LINK_STATUS,第三个参数返回0或者1,返回0代表链接失败。
第二个参数传入GL_LINFO_LOG_LENGTH,代表获取该程序的InfoLog的长度,获取到长度之后我们分配出一个char*的内存空间一获取InfoLog,函数原型如下:

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

实例:

 #ifdef DEBUG
    GLint logLength;
    glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0)
    {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetProgramInfoLog(prog, logLength, &logLength, log);
        NSLog(@"Program validate log:\n%s", log);
        free(log);
    }
#endif 

第五步:glUseProgram

调用glUseProgram方法使用这个构建出来的程序,函数原型如下:

void glUseProgram (GLuint program);

如何在iOS上搭建OpenGL ES环境

第1步. 重写layerClass方法
首先创建一个View类,继承自UIView,然后重写父类UIView的layerClass方法,并返回CAEAGLLayer类型。

 + (Class)layerClass {
    // 第1步. 重写layerClass方法
    // 只有 [CAEAGLLayer class] 类型的 layer 才支持在其上描绘 OpenGL 内容。
    return [CAEAGLLayer class];
} 

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

 - (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        //第2步. 在initWithFrame方法中获得layer并且强制类型转换为CAEAGLLayer类型的变量,同时为layer设置参数,其中包括色彩模式等属性
        CAEAGLLayer *eaglLayer = (CAEAGLLayer *)[self layer];
        // 设置描绘属性,在这里设置不维持渲染内容以及颜色格式为 RGBA8
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:NO],kEAGLDrawablePropertyRetainedBacking,kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat, nil];
        // CALayer 默认是透明的,必须将它设为不透明才能让其可见
        [eaglLayer setOpaque:YES];
        /// 放大倍数
        CGFloat scale = [UIScreen mainScreen].scale;
        [eaglLayer setContentsScale:scale];
        [eaglLayer setDrawableProperties:dict];
    }
    return self;
} 

第3步. 建立EAGL与Opengl ES的连接
创建OpenGL ES的上下文:

 //第3步. 建立EAGL与OpenGL ES的连接
    EAGLContext *_context;
    // 指定 OpenGL 渲染 API 的版本,在这里我们使用 OpenGL ES 2.0
    _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    // 设置为当前上下文
    [EAGLContext setCurrentContext:_context];
    self.context = _context; 

第4步. 建立EAGL与Layer(设备屏幕)的连接

 //第4步. 建立EAGL与Layer(设备屏幕)的连接
    //创建帧缓冲区
    glGenFramebuffers(1, &_frameBuffer);
    //创建绘制缓冲区
    glGenRenderbuffers(1, &_renderbuffer);
    //绑定帧缓冲区到渲染管线
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    //绑定绘制缓冲区到渲染管线
    glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer);
    
    //为绘制缓冲区分配存储区,此处将CAEGLLayer的绘制存储区作为绘制缓冲区的存储区
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
    
    //获取绘制缓冲区的像素宽度
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
    //获取绘制缓冲区的像素高度
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);
    
    //将绘制缓冲区绑定到帧缓冲区
    // 将 _renderbuffer 装配到 GL_COLOR_ATTACHMENT0 这个装配点上
    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
        return;
    }
    //至此我们就将EAGL与Layer(设备屏幕)连接起来了 

第5步. 绘制帧(包含:导入shader和render)

//第5步. 绘制帧
//[self render]; 

第6步. 将绘制的结果显示到屏幕上

   [_context presentRenderbuffer:GL_RENDERBUFFER];

OpenGL ES中的纹理

如何加载一张图片作为OpenGL中的纹理
首先要在显卡中创建一个纹理对象,函数原型如下:

 void glGenTextures(GLsizei n, GLuint* textures); 

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

 glGenTextures(1, &texId); 

在OpenGL ES的操作过程中必须告诉OpenGL ES具体操作的是哪一个纹理对象,所以必须调用一个绑定纹理的方法,代码如下:

 glBindTexture(GL_TEXTURE_2D, texId); 

对该纹理对象操作完毕之后,我们可以调用一次解绑定的代码:

 glBindTexture(GL_TEXTURW_2D, 0); 

定义哪个uniform采样器对应哪个纹理单元

   glUniform1i(_filterInputTextureUniform, 0);

设置纹理缩放模式

双线性过滤和邻近过滤,一般的视频渲染与处理都是使用双线性过滤,邻近过滤容易产生锯齿效果。
链接:https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/06%20Textures/

image.png

    /*
     GL_TEXTURE_MAG_FILTER和GL_TEXTURE_MIN_FILTER这两个参数指定纹理在映射到物体表面上时的缩放效果。GL_TEXTURE_MIN_FILTER是缩小情况;GL_TEXTURE_MAG_FILTER是放大情况。
     */
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

设置纹理环绕模式

链接:https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/06%20Textures/

image.png

      /*
     参数GL_TEXTURE_WRAP_S与GL_TEXTURE_WRAP_T。这两个参数分别设置纹理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);

把像素内容上传到显卡里面texId所代表的纹理对象中去:

 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData); 

绘制操作(render)/如何通过GLSL输入数据

顶点坐标
链接:https://www.jianshu.com/p/355137fa2817

顶点坐标.jpg

链接:https://www.jianshu.com/p/18d6b37363c8

image.png

纹理坐标

链接:https://www.jianshu.com/p/18d6b37363c8

image.png

链接:https://blog.csdn.net/xipiaoyouzi/article/details/53609650

image.png

// 设置窗口坐标
    glViewport(0, 0, _backingWidth, _backingHeight);
    
    // 设置清除颜色/设置背景颜色
    glClearColor(0, 1.0, 0, 1.0);//rgba
    // 清除颜色缓冲区
    glClear(GL_COLOR_BUFFER_BIT);
    
    
    GLint linkSuccess;
    glGetProgramiv(self.program, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) { //连接错误
        GLchar messages[256];
        glGetProgramInfoLog(self.program, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSLog(@"error%@", messageString);
        return ;
    }
    else {
        NSLog(@"link ok");
        glUseProgram(self.program); //成功便使用,避免由于未使用导致的的bug
    }
    //顶点坐标
        GLfloat pointArr[] = {
            0.5f, -0.5f,
            -0.5f, 0.5f,
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,
            0.5f, -0.5f,
        };
    
    //第(3)步. 加载顶点坐标
    //通过OpenGL程序句柄查找获取顶点着色器中的顶点坐标句柄
    GLuint position = glGetAttribLocation(self.program, "position");
    //绑定渲染图形所处的顶点数据
    glVertexAttribPointer(position, 2, GL_FLOAT, GL_FALSE, 0, pointArr);
    //启用指向渲染图形所处的顶点数据的句柄
    glEnableVertexAttribArray(position);
    
    //纹理坐标
    GLfloat textArr[] = {
        1.0f, 0.0f,
        0.0f, 1.0f,
        0.0f, 0.0f,
        1.0f, 1.0f,
        0.0f, 1.0f,
        1.0f, 0.0f,
    };
    //第(4)步. 加载纹理坐标
    //通过OpenGL程序句柄查找获取顶点着色器中的纹理坐标句柄
    GLuint textCoor = glGetAttribLocation(self.program, "textCoordinate");
    //绑定纹理坐标数据
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, 0, textArr);
    //启用指向纹理坐标数据的句柄
    glEnableVertexAttribArray(textCoor);
    
    //第(5)步. 加载纹理
    _filterInputTextureUniform = glGetUniformLocation(self.program, "colorMap");
    [self setupTexture:@"for_test"];
    
    //获取shader里面的变量,这里记得要在glLinkProgram后面,后面,后面!
    GLuint rotate = glGetUniformLocation(self.program, "rotateMatrix");
    
    float radians = 360 * 3.14159f / 180.0f;
    float s = sin(radians);
    float c = cos(radians);
    
    /*
     细心的开发者会发现,这里的z轴旋转矩阵和上面给出来的旋转矩阵并不一致。
     究其原因就是OpenGLES是列主序矩阵,对于一个一维数组表示的二维矩阵,会先填满每一列(a[0][0]、a[1][0]、a[2][0]、a[3][0])。
     把矩阵赋值给glsl对应的变量,然后就可以在glsl里面计算出旋转后的矩阵。
     */
    
    //z轴旋转矩阵
    GLfloat zRotation[16] = { //
        c, -s, 0, 0.0, //
        s, c, 0, 0,//
        0, 0, 1.0, 0,//
        0.0, 0, 0, 1.0//
    };
    
    //设置旋转矩阵/绑定旋转矩阵
    glUniformMatrix4fv(rotate, 1, GL_FALSE, (GLfloat *)&zRotation[0]);
    
    //使用GL_TRIANGLES方式绘制纹理
    glDrawArrays(GL_TRIANGLES, 0, 6);//三种绘制方式:GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN
    
    
    //第6步. 将绘制的结果显示到屏幕上
    [self.context presentRenderbuffer:GL_RENDERBUFFER];

glDeleteTextures

如果纹理对象不再使用,则需要将其删除,否则会造成显存的泄露。

 glDeleteTextures(1, &texId); 

Demo地址:https://github.com/ProBobo/TestOpenGLES

为什么绘制出来的三角形,矩形(图片的几何位置)的高比宽长。

链接:https://www.jianshu.com/p/31738e1136ca
答: 虽然本例中,三角形、矩形在纯数学的OpenGL ES坐标系中,长和宽是相等的。但是在本例中,帧缓存是按像素来匹配屏幕尺寸的。在渲染时候,GPU会转换 纯数学的OpenGL ES坐标系的X、Y、Z坐标为帧缓存中所对应的真实像素位置。帧缓存中的像素位置叫做视口(viewport)坐标。转换为视口坐标的后果就是:所绘制的集合图形被拉伸以适应屏幕大小,也就是高比宽大了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容