OpenGL ES案例03 - 使用GLSL完成纹理图片加载

案例:根据对GLSL语言的理解,自定义一个顶点着色器和一个片元着色器,使用着色器API完成纹理的加载。
进阶:解决纹理倒置问题。
效果如下:


翻转前效果

翻转后效果

准备工作

  1. 新建iOS应用工程,修改当前controller的view。将原来的view继承于UIView改成继承于HView。
  2. 自定义一个HVIew类,后续绘制图片在该类中完成。
  3. 新建顶点着色器文件和片元着色器文件。
    3.1 command + N,开始新建文件。
    3.2 选择iOS->Other->Empty,新建两个空文件,分别命名为:shaderv.vsh、shaderf.fsh

至此准备工作完成,接下来就开始编码工作。

自定义着色器

自定义着色器本质上其实是一个字符串,但是在Xcode的编写过程没有任何错误提示,因此,在编写过程中需要格外仔细。

  1. 顶点着色器shaderv.vsh
  • 定义两个attribute修饰符修饰的变量,分别表示顶点坐标和纹理坐标
  • 定义一个varying修饰符修饰的变量,用于将纹理坐标从顶点着色器传递给片元着色器
  • main函数,在该函数内给内建变量gl_Position赋值。若顶点坐标不需要变换,则直接将顶点坐标赋值给内建变量gl_Position。若顶点坐标需要进行变换,则将变换后的结果赋值给内建变量gl_Position。
attribute vec4 position;
attribute vec2 textCoordinate;
varying lowp vec2 varyTextCoord;
void main(){
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

2.片元着色器shaderf.fsh

  • 指定片元着色器中float类型的精度,如果不写,可能会报一些异常错误
  • 定义一个与顶点着色器桥接的纹理坐标,写法必须同在顶点着色器写法一致,否则将无法收到从顶点着色器传递过来的数据
  • 定义一个unifom修饰符修饰的变量,用于获取纹理坐标上每个像素点的纹素。
  • main函数,在函数内给内建变量gl_FragColor赋值。通过texture2D内建函数获取当前颜色值,它有两个参数:参数1:纹理图片;参数2:纹理坐标,返回值:vec4类型的颜色值。当颜色不需要进行修改时,可直接将vec4类型的颜色值赋值给内建变量gl_FragColor。当颜色需要修改时,将最终修改的结果赋值给内建变量gl_FragColor。
precision highp float;
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main(){
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

初始化

1. 创建图层

1.1 图层主要是显示OpenGL ES绘制内容的载体。它的创建有两种方式:
  • 直接使用当前view的layer。但是view的layer是继承于CALayer,需要重写类方法layerClass,使其继承于CAEAGLLayer
  • 直接使用[[CAEAGLLayer alloc] init]方法创建一个CAEAGLLayer类型的图层,并将新创建的图层添加到当前图层上。
self. myEagLayer = (CAEAGLLayer*)self.layer;
+ (Class)layerClass{
    return [CAEAGLLayer class];
}
1.2 设置scale,这里设置当前view的scale与屏幕的scale一样大
[self setContentScaleFactor:[[UIScreen mainScreen] scale]];
1.3 设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
  • kEAGLDrawablePropertyRetainedBacking:表示绘图表面显示后,是否保留其内容,true-保留,false-不保留
  • 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, kEAGLColorFormatSRGBA8, kEAGLDrawablePropertyColorFormat, nil];

2. 创建上下文

上下文主要用来保存OpenGL ES的状态,是一个状态机,不论GLKit还是GLSL,都需要使用context。
2.1 创建上下文,并指定OpenGL ES渲染API的版本号

self.myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];

2.2 设置当前上下文

[EAGLContext setCurrentContext:self.myContext];

3. 清空缓冲区

清除缓冲区的残留数据,防止其它无用数据对绘制效果造成影响

//清空渲染缓存区
glDeleteBuffers(1, &_myColorRenderBuffer);
self.myColorRenderBuffer = 0;
    
//清空帧缓存区
glDeleteBuffers(1, &_myColorFrameBuffer);
self.myColorFrameBuffer = 0;

4. 设置缓冲区

设置缓冲区包括设置RenderBuffer和FrameBuffer。

  1. RenderBuffer:是一个通过应用分配的2D图像缓冲区,需要附着在FrameBuffer上。
    1.1 RenderBuffer有3种缓冲区

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

    1.2 设置RenderBuffer

    • 定义一个缓存区ID
    • 申请一个缓冲区标志
    • 将缓冲区标识绑定到GL_RENDERBUFFER
    • 绑定一个可绘制对象(layer)的存储到一个OpenGL ES RenderBuffer对象
-(void)setupRenderBuffer{
    //1.定义一个缓存区ID
    GLuint buffer;
    //2.申请一个缓存区标志
    glGenRenderbuffers(1, &buffer);
    
    self.myColorRenderBuffer = buffer;
    
    glBindRenderbuffer(GL_RENDERBUFFER, self.myColorRenderBuffer);
    
    [self.myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myEagLayer];
}
  1. FrameBuffer:是一个收集颜色、深度、模板缓冲区的附着点,简称FBO,即是一个管理者,用来管理RenderBuffer,且FrameBuffer没有实际的存储功能,真正实现存储的是RenderBuffer。
    2.1 FrameBuffer有3个附着点

    • 颜色附着点(Color Attachment):管理纹理、颜色缓冲区
    • 深度附着点(depth Attachment):管理深度缓冲区,会根据当前深度缓冲中的值修改颜色缓冲中的内容
    • 模板附着点(Stencil Attachment):管理模板缓冲区

    2.2 设置FrameBuffer

    • 定义一个缓存区ID
    • 申请一个缓冲区标志
    • 将缓冲区标识绑定到GL_FRAMEBUFFER
    • 通过FrameBuffer来管理RenderBuffer,将RenderBuffer附着到FrameBuffer的GL_COLOR_ATTACHMENT0附着点上。
-(void)setupFrameBuffer{
    GLuint buffer;
    glGenFramebuffers(1, &buffer);
    
    self.myColorFrameBuffer = buffer;
    
    glBindFramebuffer(GL_FRAMEBUFFER, self.myColorFrameBuffer);
    
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);
}

注意点:绑定renderBuffer和FrameBuffer是有顺序的,先有RenderBuffer,才有FrameBuffer。

开始绘制

初始化

清除屏幕颜色,清空颜色缓冲区,设置视口大小。

//设置清屏颜色
glClearColor(0.3, 0.45, 0.5, 1.0);
    
//清除屏幕
glClear(GL_COLOR_BUFFER_BIT);

//1.设置视口大小
CGFloat scale = [[UIScreen mainScreen] scale];
glViewport(self.frame.origin.x * scale, self.frame.origin.y * scale, self.frame.size.width * scale, self.frame.size.height * scale);

加载自定义着色器

1. 读取并编译顶点着色程序、片元着色程序

1.1 创建一个顶点/片元着色器

*shader = glCreateShader(type);

1.2 以字符串的形式将着色器源码读取出来,并将着色器源码加载到着色器对象上

NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
const GLchar* source = (GLchar*)content.UTF8String;
glShaderSource(*shader, 1, &source, NULL);

1.3 编译着色器,把着色器源代码编译成目标代码。此时得到一个可附着到程序的着色器对象

glCompileShader(*shader);
2. 加载着色器

2.1 创建program

GLint program = glCreateProgram();

2.2 将编译好的着色器对象附着到程序中

glAttachShader(program, verShader);
glAttachShader(program, fragShader);

2.3 释放不需要的着色器对象

glDeleteShader(verShader);
glDeleteShader(fragShader);
  1. 链接program
    在链接之后可调用glGetProgramiv函数判断当前是否链接成功
glLinkProgram(self.myPrograme);
  1. 使用program
glUseProgram(self.myPrograme);

设置并处理顶点数据

  1. 设置顶点数据
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,
};
  1. 申请一个顶点缓冲区ID,并将它绑定到GL_ARRAY_BUFFER标识符上
GLuint attrBuffer;
glGenBuffers(1, &attrBuffer);
glBindBuffer(GL_ARRAY_BUFFER, attrBuffer);
  1. 把顶点数据从CPU拷贝到GPU
glBufferData(GL_ARRAY_BUFFER, sizeof(attrArr), attrArr, GL_DYNAMIC_DRAW);
  1. 打开顶点/片元着色器属性通道
  • 通过glGetAttribLocation函数获取顶点属性入口,它需要两个参数,参数1:program;参数2:自定义着色器文件中变量名称的字符串,重点:这里的字符串必须同自定义着色器文件中变量名称保持一致
  • 通过glEnableVertexAttribArray函数打开着色器的属性通道
  • 通过glVertexAttribPointer函数设置读取方式
//设置顶点坐标
GLuint position = glGetAttribLocation(self.myPrograme, "position");
glEnableVertexAttribArray(position);
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), NULL);

//设置纹理坐标
GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
glEnableVertexAttribArray(textCoor);
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), (float *)NULL + 3);

加载纹理

加载纹理的过程是将png/jpg图片解压缩成位图,并通过自定义着色器读取每个像素点的纹素。

  1. 解压缩png/jpg图片,将UIImage转换为CGImageRef。
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
  1. 根据CGImageRef属性获取图片的宽和高,并开辟一段空间用于存放解压缩后的位图信息。位图数据的大小为宽4。为什么是宽4?因为图片共有宽高个像素点,每个像素点有4个字节,即RGBA,因此共有宽高*4大小的空间。
//读取图片的大小,宽和高
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);

//获取图片字节数 宽*高*4(RGBA)
GLubyte * spriteData = (GLubyte *) calloc(width * height * 4, sizeof(GLubyte));
  1. 创建CGContextRef上下文
/*
 参数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);
  1. 在CGContextRef上将图片绘制出来,调用CGContextDrawImage函数,使用默认方式绘制
/*
 CGContextDrawImage 使用的是Core Graphics框架,坐标系与UIKit 不一样。UIKit框架的原点在屏幕的左上角,Core Graphics框架的原点在屏幕的左下角。
 CGContextDrawImage 
 参数1:绘图上下文
 参数2:rect坐标
 参数3:绘制的图片
*/
CGContextDrawImage(spriteContext, rect, spriteImage);
  1. 绘制完成之后,需要将上下文释放掉
CGContextRelease(spriteContext);
  1. 经过重绘之后,就将jpg/png图片转换成了位图得到了纹理数据。接下来就是载入纹理数据。
    6.1 绑定纹理到默认的纹理ID
    6.2 设置纹理属性
    6.3 载入2D纹理数据
//绑定纹理到默认的纹理ID
glBindTexture(GL_TEXTURE_2D, 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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);

//释放spriteData
free(spriteData); 
  1. 设置纹理采样器
    主要是用来获取纹理中对应像素点的颜色值,即纹素。
  • 通过glGetUniformLocation函数获取片元着色器中uniform的入口。该函数需要传入两个参数,参数1:program;参数2:在片元着色器中用uniform修饰的变量名字的字符串。注意,该字符串必须同片元着色器中对应的变量名保持一致
  • 使用glUniform1i函数获取纹素,它也有两个参数,参数1:片元着色器中uniform的入口;参数2:纹理ID,默认为0。
glUniform1i(glGetUniformLocation(self.myPrograme, "colorMap"), 0);

绘制

开始绘制,存储到RenderBuffer,从RenderBuffer将图片显示到屏幕上。

  • 调用glDrawArrays函数,指定图元连接方式进行绘制
  • context调用presentRenderbuffer函数将绘制好的图片渲染到屏幕上显示
glDrawArrays(GL_TRIANGLES, 0, 6);
[self.myContext presentRenderbuffer:GL_RENDERBUFFER];

至此,使用GLSL加载纹理已经完成,完整代码见Demo地址;

从效果图上看到,图片呈倒立显示,这是因为OpenGL要求原点(0,0)位于图片的左下角,Y坐标从下往上增加,而图片纹理的原点(0,0)是位于图片的左上角,Y坐标从上往下增加。所以最后的照片呈上下倒置的效果。
以下是几种解决方案:

  • 方案1:将顶点绕Y轴进行翻转。这样可以实现正常显示。
    问题:如何实现绕Y轴翻转
    解决:将顶点坐标与一个旋转矩阵相乘,得到的结果就是翻转之后的顶点坐标。
    重点:在3D课程中用的是横向量,在OpenGL ES用的是列向量。顶点坐标是一个1行4列的矩阵,因此,旋转矩阵必须是4行4列,这样相乘之后才能得到新的1行4列的顶点坐标。另外,要实现翻转,只需要将该方向的坐标数据进行反向,如当前需要沿X轴反向,只需要将X轴的数据全部*-1,即可将X轴的数据翻转。
    代码详见方案1代码
  • 方案2:可以解压缩图片的时候对图片进行翻转。
    解决:在context绘制的图片,对图片进行翻转。
    重点:由于翻转之后,顶点数据的坐标会发生变化,超过绘制的区域,因此在翻转之后需要将顶点移至绘制区域内。
    主要使用的函数有
//先平移至合适的位置,也可以在翻转之后再移至绘制区域内
CGContextTranslateCTM(context, 0, height);
//将Y轴翻转
CGContextScaleCTM(context, 1, -1);

代码详见方案2代码

  • 方案3:修改片元着色器纹理坐标,将片元着色器中的纹理坐标在Y轴方向翻转。
    重点:如何获取纹理坐标的Y轴方向数据,通过'varyTextCoord.y'即可得到Y轴数据。将1.0-varyTextCoord.y即可实现翻转。
vec2 newCoord = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
gl_FragColor = texture2D(colorMap, newCoord);

代码详见方案3代码

  • 方案4:修改顶点着色器纹理坐标,将顶点着色器的纹理坐标在Y轴方向翻转。
    该方案原理同方案3一样,只是在不同的着色器完成纹理坐标的翻转。
    代码详见方案4代码

  • 方案5:修改源顶点数据中顶点坐标和纹理坐标的映射关系。
    原理同方案3、4一致,只是直接在顶点数组中修改源数据。
    原顶点数据数组

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,
};

修改后的顶点数组

GLfloat attrArr[] ={
    0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
    -0.5f, -0.5f, -1.0f,    0.0f, 1.0f,
        
    0.5f, 0.5f, -1.0f,      1.0f, 0.0f,
    -0.5f, 0.5f, -1.0f,     0.0f, 0.0f,
    0.5f, -0.5f, -1.0f,     1.0f, 1.0f,
};

代码详见方案5代码

你可能感兴趣的:(OpenGL ES案例03 - 使用GLSL完成纹理图片加载)