八、OpenGL ES - GLSL的使用

音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总

GLKit仅可以实现有限的功能,且无法自定义。我们可以通过GLSL写顶点着色器和片元着色器,完成自定义的操作,这是OpenGL ES实现可编程管道的手段。因此本文就着重介绍GLSL语法,以及GLSL的初步使用。

主要内容:
1、GLSL语法
2、常用API
3、GLSL渲染流程
4、案例 - 图片的加载

1、GLSL语法

这里只介绍最基础语法,更多的语法可以查看OpenGL ES着色语言

1.1 数据类型

1.1.1 向量数据类型

一般就用vec2,vec3,vec4


向量数据类型.png
1.1.2 矩阵数据类型

mat列*行
一般是mat3和mat4

矩阵数据类型.png

1.2 变量存储限定符

变量存储限定符.png

1.3 OpenGL ES错误处理

XCode没有专门编写着色器的文件的,也不能进行错误提示,无法使用断点查看调试。
如果不正确使用OpenGL ES命令,应用程序就会产生一个错误编码,这个错误编码将会被记录,我们可以使用glGetError查询。
下面是查询结果对应的信息。


查询结果.png

1.4 着色器的简单写法

着色器其实就是一个字符串。
因为XCode没有专门编写着色器的文件,我们通过空的文本文件修改后缀后作为着色器使用。
这个文件需要我们自己进行编译和链接,所以无需区分后缀,后缀仅让开发者自己看的比较清晰即可。

下面对简单的着色器的语句进行逐行解读。

1.4.1 顶点着色器(vertext shader)
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;

void main()
{
    varyTextCoord = textCoordinate;
    gl_Position = position;
}
  1. vec4是向量数据类型的一种,是四分量的浮点型
  2. attribute表示属性,只能传递给顶点着色器,修饰的数据:顶点、纹理、颜色、法线等
  3. varying表示需要传递的变量,当需要将顶点着色器的数据传递到片元着色器时,两个着色器中一模一样的纹理坐标变量就需要它来修饰
  4. lowp是低精度,highp是高精度
  5. position是顶点数据
  6. textCoordinate是纹理顶点数据,顶点着色器不需要,但是需要通过它来传递给片元着色器
  7. varyTextcoord是需要传递给片元着色器的一致的变量
  8. varyTextCoord = textCoordinate;将纹理顶点数据传递给片元着色器
  9. gl_Position = position;顶点着色器最终都是用内建变量gl_Position接收顶点数据
1.4.2 片元着色器(fragmengt)
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;

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

}
  1. precision highp float;表示将float的默认精度设置为高精度(recision 表示精度)
  2. varyTextCoord用来接收顶点着色器传递过来的纹理顶点数据
  3. 因为用sampler2D修饰,说明colorMap表示传递的纹理数据
  4. gl_FragColor就是用来接收纹理数据的。
  5. texture2D(colorMap,varyTextCoord)用来获取纹理对应坐标下的纹素。纹素是纹理对应像素点的颜色值

2、常用API

这里不单独再写,案例中我所列出来的API都是常用的

3、GLSL渲染流程

这里不单独再写,案例中的实现思路就是渲染流程
后续画图。

4、案例 - GLSL加载图片

案例地址:GLSL使用基本流程

效果:


效果.png

4.1 简单介绍

实现功能:

  1. GLSL实现图片加载
  2. 图片翻转

实现思路:

  1. 初始化
    1. 创建图层
    2. 创建上下文环境
    3. 清空缓存区
    4. 设置RenderBuffer、FrameBuffer
  2. 着色器的使用
    1. 着色器程序的编写
    2. 着色器的编译
    3. 链接到程序对象中
    4. 程序对象的连接
    5. 程序对象的开启
  3. 顶点/纹理坐标数据的传递
    1. 获取数据
    2. 在显存中创建缓存区并拷贝数据
    3. 获取传递数据的通道的ID
    4. 开启通道ID
    5. 设置数据传递的方式(也就是获取缓存区中的哪部分数据来进行传递)
  4. 纹理数据的获取
    1. 解析图片获取到图片数据
    2. 重绘图片(注意此时是反的)
    3. 载入纹理数据
  5. 纹理数据的传递
    1. 在第4步以及获取到纹理数据
    2. 获取传递数据的通道
    3. 将纹理传递到片元着色器
  6. 绘图并渲染上屏
    1. 绘图
    2. 渲染上屏

4.2 创建图层

  1. 创建特殊图层,CAEAGLLayer是用来绘制内容的图层,相当于画板,继承自CALayer
  2. 设置描述属性
-(void)setupLayer
{
    //1.创建特殊图层
    /*
     重写layerClass,将CCView返回的图层从CALayer替换成CAEAGLLayer
     */
    self.myEagLayer = (CAEAGLLayer *)self.layer;
    
    //2.设置scale,缩放因子,设置当前View的比例因子
    [self setContentScaleFactor:[[UIScreen mainScreen]scale]];


    //3.设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
    /*
     kEAGLDrawablePropertyRetainedBacking  表示绘图表面显示后,是否保留其内容。
     kEAGLDrawablePropertyColorFormat
         可绘制表面的内部颜色缓存区格式,这个key对应的值是一个NSString指定特定颜色缓存区对象。默认是kEAGLColorFormatRGBA8;
     
         kEAGLColorFormatRGBA8:32位RGBA的颜色,4*8=32位
         kEAGLColorFormatRGB565:16位RGB的颜色,
         kEAGLColorFormatSRGBA8:sRGB代表了标准的红、绿、蓝,即CRT显示器、LCD显示器、投影机、打印机以及其他设备中色彩再现所使用的三个基本色素。sRGB的色彩空间基于独立的色彩坐标,可以使色彩在不同的设备使用传输中对应于同一个色彩坐标体系,而不受这些设备各自具有的不同色彩坐标的影响。


     */
    self.myEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];

}

创建特殊图层
1、可以通过CAEAGLLayer直接创建,并add到当前view的layer上
2、也可以像本案例中这样直接将当前view的layer强转成CAEAGLLayer

注意:

  1. GLSL中使用的图层是CAEAGLLayer,它用来绘制内容的图层,相当于画板,继承自CALayer
  2. CAEAGLLayer的实现需要重写layerClass,并且将CCView返回的图层从CALayer替换成CAEAGLLayer
  3. 可以通过layer的drawableProperties来设置渲染内容的属性

需掌握API:
CAEAGLLayer:GLSL的绘制图层

4.3 设置上下文环境

和通过GLKit方式一样,就不赘言了

  1. 指定渲染版本
  2. 创建上下文
  3. 设置为当前上下文
-(void)setupContext
{
    //1.指定OpenGL ES 渲染API版本,我们使用2.0
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    //2.创建图形上下文
    EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
    //3.判断是否创建成功
    if (!context) {
        NSLog(@"Create context failed!");
        return;
    }
    //4.设置图形上下文
    if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"setCurrentContext failed!");
        return;
    }
    //5.将局部context,变成全局的
    self.myContext = context;
    
}

4.4 设置缓存区

过程:

  1. 清空渲染、帧缓存区
  2. 设置渲染缓存区
  3. 设置帧缓存区

注意:

  1. buffer分为frame buffer 和 render buffer2个大类。
  2. RenderBuffer:是一个通过应用分配的2D图像缓冲区,render buffer则又可分为3类。colorBuffer、depthBuffer、stencilBuffer。需要附着在FrameBuffer上
  3. FrameBuffer:是一个收集颜色、深度和模板缓存区的附着点,简称FBO,即是一个管理者,用来管理RenderBuffer,简称FBO。
  4. FrameBuffer没有实际的存储功能,真正实现存储的是RenderBuffer,RenderBuffer需要附着到FrameBuffer,供FrameBuffer管理

关系图:


FBO、RBO 、Textures的关系图.png

FrameBuffer:

  • 颜色附着点(Color Attachment):管理纹理、颜色缓冲区
  • 深度附着点(depth Attachment):管理深度缓冲区(Depth Buffer),会影响颜色缓冲区。
  • 模板附着点(Stencil Attachment):管理模板缓冲区(Stencil Buffer)

RenderBuffer

  • 深度缓存区(Depth Buffer):存储深度值等
  • 颜色缓存区:存储纹理坐标中对应的纹素(纹理像素的色值)、颜色值等
  • 模板缓存区(Stencil Buffer):存储模板
4.4.1 清空缓存区

清理缓冲区的目的在于清除残留数据,防止残留数据对本次操作造成影响

  1. 清空该缓存区ID的缓存
  2. 将自定义的属性的buffer ID初始化一下
    glDeleteBuffers(1, &_myColorRenderBuffer);//清空buffer
    self.myColorRenderBuffer = 0;//初始化
    
    glDeleteBuffers(1, &_myColorFrameBuffer);
    self.myColorFrameBuffer = 0;

重要API:

清空缓存区:
glDeleteBuffers(1, &_myColorRenderBuffer);
参数1:只有一个纹理
参数2:缓存区ID

4.4.2 设置RenderBuffer

开辟渲染缓存区就和之前所讲的顶点缓存区基本一样,他们都是在显存中开辟空间,操作方式是一样的。

-(void)setupRenderBuffer
{
    //1.定义一个缓存区ID
    GLuint buffer;
    
    //2.申请一个缓存区标志
    glGenRenderbuffers(1, &buffer);
    
    //3.
    self.myColorRenderBuffer = buffer;
    
    //4.将标识符绑定到GL_RENDERBUFFER,此时RenderBuffer中已经开辟了。
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    
    
    //5.将可绘制对象drawable object's  CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
    
    
}

重要API:

glGenRenderbuffers:
申请开辟一段缓存区,并给其增加一个标识,也就是增加一个名字
参数1:申请第几个
参数2:表示缓存区标识

glBindRenderbuffer:

绑定缓存区,其实就是声明该缓存区的作用
参数1:表明缓存区的作用,设置宏定义GL_RENDERBUFFER,表示该缓存区为渲染缓存区
参数2:缓存区标识,代表这个缓存区区域

renderbufferStorage:
将上下文context与layer绑定,并且说明存储的是渲染缓存区
参数1:表示缓存区的类型
参数2:表示layer

4.4.3 设置FrameBuffer

开辟帧缓存区的方式和渲染缓存区基本一样,只是需要将renderbuffer跟framebuffer进行绑定

-(void)setupFrameBuffer
{
    //1.定义一个缓存区ID
    GLuint buffer;
    
    //2.申请一个缓存区标志
    //glGenRenderbuffers(1, &buffer);
    //glGenFramebuffers(1, &buffer);
    glGenBuffers(1, &buffer);
    
    //3.
    self.myColorFrameBuffer = buffer;
    
    //4.
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    
    /*生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
     调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用
     */
    
    //5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
    
}

注意:

  1. 申请空间可以写glGenFramebuffers,也可以用glGenBuffers
  2. 绑定帧缓存区的宏定义是GL_FRAMEBUFFER
  3. 需要将渲染缓存区绑定到帧缓存上

重要API:

glFramebufferRenderbuffer:
将渲染缓存区绑定到帧缓存区上
参数1:帧缓存区类型
参数2:是附着点,此处写的是附着到颜色缓存区
参数3:渲染缓存区类型
参数4:渲染缓存区ID

4.3 着色器的编译和链接

着色器编译和链接、程序对象的链接和使用是固定的操作步骤,记住每步的流程就可以了。

  1. 创建shader对象
  2. 将着色器源码设置到shader对象上
  3. 编译Shader
  4. 链接到program上
  5. program进行链接
  6. 通过glUseProgram开启program
-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
    //1.定义2个临时着色器对象
    GLuint verShader, fragShader;
    //创建program
    GLint program = glCreateProgram();
    
    //2.编译顶点着色程序、片元着色器程序
    //参数1:编译完存储的底层地址
    //参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
    //参数3:文件路径
    //&可以取地址符,这里verShader本来是变量本身,而这里需要传入的是地址,所以需要通过&取地址
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    //3.将编译后的着色器对象链接到程序对象
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    //4.释放不需要的shader
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}

//编译shader
/*
 1、创建shader
 2、将着色器源码加载到着色器对象中
 3、编译
 */
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
    
    //1.读取文件内容的字符串
    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    //转成C语言字符串
    const GLchar* source = (GLchar *)[content UTF8String];
    
    //2.创建一个shader(根据type类型)
    *shader = glCreateShader(type);
    
    //3.将着色器源码附加到着色器对象上。
    //参数1:shader,要编译的着色器对象 *shader
    //参数2:numOfStrings,传递的源码字符串数量 1个
    //参数3:strings,着色器程序的源码(真正的着色器程序源码)
    //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
    glShaderSource(*shader, 1, &source,NULL);
    
    //4.把着色器源代码编译成目标代码
    glCompileShader(*shader);
    
    
}

    //3.加载shader
    self.myPrograme = [self loadShaders:vertFile Withfrag:fragFile];
    
    //4.链接
    glLinkProgram(self.myPrograme);
    GLint linkStatus;
    //获取链接状态
    glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLchar message[512];
        glGetProgramInfoLog(self.myPrograme, sizeof(message), 0, &message[0]);
        NSString *messageString = [NSString stringWithUTF8String:message];
        NSLog(@"Program Link Error:%@",messageString);
        return;
    }
    
    NSLog(@"Program Link Success!");
    //5.使用program
    glUseProgram(self.myPrograme);

注:这部分代码不属于业务逻辑,只是固定步骤,以后就按照这样来写就可以

重要API:

1、创建shader对象
glCreateShader:
用来创建着色器对象

参数1:shader类型,可以传入GL_VERTEX_SHADER、GL_FRAGMENT_SHADER代表创建的是顶点着色器或片元着色器。

2、将着色器源码添加到shader对象上
glShaderSource:
着色器源码是作为字符串添加进入的,用以设置着色器对象

参数1:shader,要编译的着色器对象 *shader
参数2:numOfStrings,传递的源码字符串数量 这里是1个(一般都是1)
参数3:strings,着色器程序的源码(真正的着色器程序源码)
参数4:lenOfStrings,长度,具有每个字符串长度的数组,(也可以直接写NULL,这意味着字符串是NULL终止的,系统会自行判断长度)

3、对着色器进行一下编译
着色器对象的编译
glCompileShader( * shader)

4、将编译后的着色器对象链接到程序对象
glAttachShader(program, verShader);
前边先进行着色器的编译,之后链接到程序对象中,这样程序对象就可以使用着色器了。

参数1:program表示程序对象,最终编译链接后的结果就是程序对象,后面就可以通过它来对着色器进行处理。
参数2:verShader,表示链接到程序对象的着色器

5、删除不需要的shader:
glDeleteShader(verShader);
删除的是着色器对象,当这个着色器对象编译完成后并链接到程序对象中,说明已经不需要了,就可以将其删除释放空间

参数1:就是需要释放的着色器对象

6、创建程序对象program:
GLint program = glCreateProgram();

7、链接程序对象:
glLinkProgram(self.myPrograme);
参数:程序对象,该对象已经将着色器对象链接进来了。

8、获取链接状态
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
参数1:程序对象
参数2:传入GL_LINK_STATUS,表示我们想要得到的是链接状态
参数3:连接状态,作为返回值,可能为失败,可能为成功

9、使用program
glUseProgram(self.myPrograme);
在使用program前,必须要先用glUseProgram设置一下,相当于启动的过程。
参数1:程序对象

4.3 获取顶点/纹理坐标数据

过程:

  1. 设置顶点数据:主要是初始化顶点坐标和纹理坐标
  2. 开辟顶点缓存区:用于将顶点数据从内存拷贝至显存中
  3. 设置顶点/片元的通道

顶点数据从内存拷贝到显存中的过程与GLKView方式一样,设置读取方式也一样
区别在于打开读取通道时需要我们自己指定通道。

代码:

    //6.设置顶点、纹理坐标
    //前3个是顶点坐标,后2个是纹理坐标
    GLfloat attrArr[] =
    {
        0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
        -0.5f, -0.5f, -1.0f,    0.0f, 0.0f,
        
        0.5f, 0.5f, -1.0f,      1.0f, 1.0f,
        -0.5f, 0.5f, -1.0f,     0.0f, 1.0f,
        0.5f, -0.5f, -1.0f,     1.0f, 0.0f,
    };
    
    
    //7.-----处理顶点数据--------
    //(1)顶点缓存区
    GLuint attrBuffer;
    //(2)申请一个缓存区标识符
    glGenBuffers(1, &attrBuffer);
    //(3)将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    //(4)把顶点数据从CPU内存复制到GPU上
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);

    //8.将顶点数据通过myPrograme中的传递到顶点着色程序的position
    //1.glGetAttribLocation,用来获取vertex attribute的入口的.
    //2.告诉OpenGL ES,通过glEnableVertexAttribArray,
    //3.最后数据是通过glVertexAttribPointer传递过去的。
    
    //(1)注意:第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    
    //(2).设置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(position);
    
    //(3).设置读取方式
    //参数1:index,顶点数据的索引
    //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
    //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
    //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
    //参数5:stride,连续顶点属性之间的偏移量,默认为0;
    //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
    
    
    //9.----处理纹理数据-------
    //(1).glGetAttribLocation,用来获取vertex attribute的入口的.
    //注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
    GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
    
    //(2).设置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(textCoor);
    
    //(3).设置读取方式
    //参数1:index,顶点数据的索引
    //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
    //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
    //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
    //参数5:stride,连续顶点属性之间的偏移量,默认为0;
    //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);

重要API:

1、顶点缓存区的设置

因为这里的代码已经比较熟悉了,所以就整体解读一下,不详细说明了

    //(1)顶点缓存区
    GLuint attrBuffer;
    //(2)申请一个缓存区标识符
    glGenBuffers(1, &attrBuffer);
    //(3)将attrBuffer绑定到GL_ARRAY_BUFFER标识符上
    glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
    //(4)把顶点数据从CPU内存复制到GPU上
    /*
    参数1:缓存区类型
    参数2:拷贝的数据的大小
    参数3:需要拷贝的数据
    参数4:表示动态绘制,"Draw”意味着数据之后将会被送往GPU进行绘制
    */
    glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);

解读:

  1. 首先要申请缓存区空间,之后给该缓存区空间设置类型(这里使用的类型是数组类型),最后获取数据。
  2. glGenBuffers是开辟了一个空间,并为其命名。
  3. GL_DYNAMIC_DRAW表示缓存区的数据之后将会被送往GPU进行绘制操作。可以用静态绘制,也可以是动态绘制。
2、打开读取通道:
    //获取着色器中的某个通道
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    //打开这个通道
    glEnableVertexAttribArray(position);

glGetAttribLocation(self.myPrograme, "position")
它用来获取着色器中的某个通道

参数1:程序对象
参数2:属性通道名称,也就是通过这个属性通道来传递信息
最终返回属性通道ID

注:这里的属性通道要与着色器中将要作为接收数据的那个字符串名称保持一致

glEnableVertexAttribArray(position);
用来开启这个通道
参数1:属性通道ID

3、设置读取方式:

glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);
设置读取方式,也就是怎样可以从显存中读取到正确的数据

参数1:index,属性通道ID
参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.顶点数组有三个顶点,所以是3
参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
参数5:stride,连续顶点属性之间的偏移量,默认为0; 也就是下一个顶点的起始位置到上一个顶点起始位置的距离
参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0

注:纹理坐标数据的传递与顶点坐标一样

4.3 加载纹理

过程:

  1. 图片解析
  2. 图片重绘
  3. 获取纹理数据
    1). 绑定纹理
    2). 设置纹理参数
    3). 加载纹理

代码:

- (GLuint)setupTexture:(NSString *)fileName {
    
    //1、将 UIImage 转换为 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    //判断图片是否获取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    //2、读取图片的大小,宽和高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    //3.获取图片字节数 宽*高*4(RGBA)
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    //4.创建上下文
    /*
     参数1:data,指向要渲染的绘制图像的内存地址
     参数2:width,bitmap的宽度,单位为像素
     参数3:height,bitmap的高度,单位为像素
     参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
     参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
     参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
     */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    

    //5、在CGContextRef上--> 将图片绘制出来
    /*
     CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
     CGContextDrawImage 
     参数1:绘图上下文
     参数2:rect坐标
     参数3:绘制的图片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
   
    //6.使用默认方式绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
   
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
    CGContextDrawImage(spriteContext, rect, spriteImage);
   
    //7、画图完毕就释放上下文
    CGContextRelease(spriteContext);
    
    //8、绑定纹理到默认的纹理ID(
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //9.设置纹理属性
    /*
     参数1:纹理维度
     参数2:线性过滤、为s,t坐标设置模式
     参数3:wrapMode,环绕模式
     */
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    float fw = width, fh = height;
    
    //10.载入纹理2D数据
    /*
     参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
     参数2:加载的层次,一般设置为0
     参数3:纹理的颜色值GL_RGBA
     参数4:宽
     参数5:高
     参数6:border,边界宽度
     参数7:format
     参数8:type
     参数9:纹理数据
     */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    //11.释放spriteData
    free(spriteData);   
    return 0;
}

重要API:

1、图片解析
  1. 将一张UIImage图片解析出图片的一些参数,比如图片宽高、图片的颜色空间。
  2. 之后返回图片的上下文和图片数据。
  3. 最后分别使用图片上下文重绘图片、通过图片数据传递到片元着色器中。
//1、将 UIImage 转换为 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
    
    //判断图片是否获取成功
    if (!spriteImage) {
        NSLog(@"Failed to load image %@", fileName);
        exit(1);
    }
    
    //2、读取图片的大小,宽和高
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    //3.开辟内存空间以存放图片数据
    GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
    
    //4.创建上下文
    获取到图片数据,并返回上下文
    /*
     参数1:data,指向要渲染的绘制图像的内存地址
     参数2:width,bitmap的宽度,单位为像素
     参数3:height,bitmap的高度,单位为像素
     参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
     参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
     参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
     */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4,CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
2、图片重绘:

通过CGContextRef进行重绘

/*
     CGContextDrawImage 
     参数1:绘图上下文
     参数2:rect坐标
     参数3:绘制的图片
     */
    CGRect rect = CGRectMake(0, 0, width, height);
   
    //6.使用默认方式绘制
    CGContextDrawImage(spriteContext, rect, spriteImage);
   
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);
    CGContextScaleCTM(spriteContext, 1.0, -1.0);
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);
    CGContextDrawImage(spriteContext, rect, spriteImage);

注:
CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。也就是说通过这样绘制出来的是倒置的。所以需要图片倒置。

图片倒置的方案和原理可以看Style_月月的博客https://www.jianshu.com/p/102d1f8f20eb

通常来说可以在重绘图片时进行倒置,这样无需处理着色器,性能更好

翻转过程:


翻转过程.png

代码注解:

  • 平移画布,沿着x/y坐标进行平移
    CGContextTranslateCTM(spriteContext, rect.origin.x, rect.origin.y);

  • 平移画布,在y轴上向下平移一个图片高度大小
    CGContextTranslateCTM(spriteContext, 0, rect.size.height);

  • 缩放画布,x轴上为1.0,表示不变,y轴上为-1.0表示进行上下翻转(这也就是为什么上面要平移一个图片高度大小)
    CGContextScaleCTM(spriteContext, 1.0, -1.0);

  • 平移画布,沿着x/y坐标进行平移回到原点位置
    CGContextTranslateCTM(spriteContext, -rect.origin.x, -rect.origin.y);

  • 【总结】图片解压缩的一般步骤
    • 将UIImage 转换为CGImageRef & 判断图片是否获取成功
    • 获取图片的大小,宽和高
    • 开辟图片数据的空间
    • 创建CGContextRef上下文,并给图片数据空间填充数据
    • 使用CGContextDrawImage绘制图片
    • 进行图片翻转
3、获取纹理数据

具体使用过程和OpenGL中基本是一样的。只是一些方法调用不一样

绑定纹理:
glBindTexture(GL_TEXTURE_2D, 0);
参数1:绑定的纹理类型
参数2:纹理ID,如果只有一个纹理的话,就直接用0即可,如果有多个,需要区分设置

设置纹理属性:
具体的属性类型以及效果可以查看文档纹理

此处采用线性过滤和带有拉伸效果(这都是随便写的)

/*
     参数1:纹理维度
     参数2:线性过滤、为s,t坐标设置模式
     参数3:wrapMode,环绕模式
     */
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

载入纹理:
载入的纹理数据就是刚才通过解析图片获取到的。
每个参数一看便知,不再赘言

/*
     参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
     参数2:加载的层次,一般设置为0
     参数3:纹理的颜色值GL_RGBA
     参数4:宽
     参数5:高
     参数6:border,边界宽度
     参数7:format
     参数8:type
     参数9:纹理数据
     */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);

4.3 传递纹理数据到着色器中

获取到纹理数据后就可以传递到片元着色器中了
是将纹理中的每个像素点对应的颜色值,即纹素传入到片元着色器中。

    //11. 设置纹理采样器 sampler2D
    glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);

glGetUniformLocation(self.myPrograme, "colorMap")
获取到通道ID
参数1:程序对象
参数2:通道名称,也就是指示片元着色器中作为通道的那个变量字符串
注:为了能够获取到正确的通道ID,通道名称必须填写与着色器中的名称保持一致

glUniform1i
传递纹素到片元着色器
参数1 :入口通道ID,通过它作为通道来传递纹素
参数2:纹理ID,这里只有一个纹理,所以用默认的ID(0)

4.4 绘制

开始绘制,并渲染上屏

代码:

    //12.绘图
    glDrawArrays(GL_TRIANGLES, 0, 6);
    
    //13.从渲染缓存区显示到屏幕上
    [self.myContext presentRenderbuffer:GL_RENDERBUFFER];

重要API:
glDrawArrays(GL_TRIANGLES, 0, 6);
glDrawArrays指定图元连接方式进行绘制
参数1:图元类型,这里是三角形
参数2:有几层
参数3:有几个顶点

presentRenderbuffer
渲染上屏,也就是将图片渲染到屏幕上执行每次重新渲染都要调一次这个方法
传入的参数是GL_RENDERBUFFER,表示渲染缓存区

4.5 自定义着色器的编写

本身这个是比较重要的,但是上文基本已经详细解释过了,所以就放在最后说明

自定义的着色器本质上其实是一个字符串,且在Xcode中编写时,是没有任何提示的,所以需要格外仔细!

4.5.1 顶点着色器

代码:

//顶点坐标
attribute vec4 position;
纹理坐标
attribute vec2 textCoordinate;
//纹理坐标
varying lowp vec2 varyTextCoord;

void main(){
    //通过varying 修饰的varyTextCoord,将纹理坐标传递到片元着色器
    varyTextCoord = textCoordinate;
    //给内江变量gl_Position赋值
    gl_Position = position;
}

重点:

  1. 在顶点着色器中既要创建接收纹理坐标的变量,也要创建传递给片元着色器的变量
  2. 因为需要将纹理坐标通过顶点着色器传递给片元着色器,所以需要在这里进行赋值
4.5.2 片元着色器

代码:

//指定float的默认精度
precision highp float;
//纹理坐标
varying lowp vec2 varyTextCoord;
//纹理采样器(获取对应的纹理ID)
uniform sampler2D colorMap;

void main(){
    //texture2D(纹理采样器,纹理坐标),获取对应坐标纹素
    //纹理坐标添加到对应像素点上,即将读取的纹素赋值给内建变量 gl_FragColor
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

重点:

  1. varying lowp vec2 varyTextCoord;是顶点着色器过来的,所以它必须与顶点着色器中的保持一致
  2. texture2D(colorMap, varyTextCoord)是返回每个像素点的颜色,也就是纹素。

你可能感兴趣的:(八、OpenGL ES - GLSL的使用)