OpenGL ES 入门之旅(13) -- GLSL加载图片案例

1,978 阅读15分钟

在上篇文章GLSL初始着色器语言中已经介绍过如何编写一个着色器文件,以及如何连接程序对象和着色器的对象的方法函数,那么接下来通过一个实际的案例来看一下这些方法的具体使用。

在学习案例之前,先来看一下什么是FrameBuffer和RenderBuffer?

帧缓冲区对象FrameBuffer(FBO)

在OpenGL渲染管线中,几何数据和纹理经过多次转化和多次测试,最后以二维像素的形式显示在屏幕上。OpenGL管线的最终渲染目的地被称作帧缓冲(framebuffer)。帧缓冲是一些二维数组和OpenG所使用的存储区的集合:颜色缓冲、深度缓冲、模板缓冲和累计缓冲。默认情况下,OpenGL将帧缓冲区作为渲染最终目的地。此帧缓冲区完全由系统生成和管理。

我们知道,在应用程序调用任何的OpenGL ES命令之前,需要首先创建一个渲染上下文和绘图表面,并使之成为现行上下文和表面,之前在渲染的时候,其实一直使用的是原生窗口系统(比如EAGL,GLFW)提供的渲染上下文和绘图表面(即帧缓冲区)。 一般情况下,我们只需要系统提供的帧缓冲区作为绘图表面,但是又有些特殊情况,比如阴影贴图、动态反射、处理后特效等需要渲染到纹理操作的,如果使用系统提供的帧缓冲区,效率会比较低低下,因此需要自定义自己的帧缓冲区。

帧缓冲区对象API支持如下操作: ·仅使用OpenGL ES 命令创建帧缓冲区对象 ·在单一EGL上下文中创建和使用多个缓冲区对象,也就是说,不需要每一个帧缓冲区都有一个渲染上下文。 ·创建屏幕外颜色,深度或者模板渲染缓冲区和纹理,并将它们链接到帧缓冲区对象 ·在多个帧缓冲区之间共享颜色,深度或者模板缓冲区 ·将纹理直接链接到帧缓冲区作为颜色或者深度,从而避免了进行复制操作的必要 ·在帧缓冲区之间复制并使帧缓冲区内容失效。

创建帧缓冲区对象

//定义一个缓存区ID
GLuint buffer;
//申请一个缓存区标志
glGenFramebuffers(1, &buffer);
// 然后绑定
glBindFramebuffer(GL_FRAMEBUFFER, buffer);

glGenFramebuffers (GLsizei n, GLuint* framebuffers) : 第一个参数是要创建的帧缓存的数目, 第二个参数是指向存储一个或者多个ID的变量或数组的指针。 它返回未使用的帧缓冲区对象的ID。ID为0表示默认帧缓存,即系统提供的帧缓存。 一旦一个FBO被创建,在使用它之前必须绑定 glBindFramebuffer (GLenum target, GLuint framebuffer): 第一个参数target是GL_FRAMEBUFFER, 第二个参数是帧缓冲区对象的ID。 一旦帧缓冲区对象被绑定,之后的所有的OpenGL操作都会对当前所绑定的帧缓冲区对象造成影响。ID为0表示缺省帧缓存,即默认的系统提供的帧缓存。因此,在glBindFramebuffer()中将ID设置为0可以解绑定当前帧缓冲区对象。

在绑定到GL_FRAMEBUFFER目标之后,所有的读取和写入帧缓冲的操作将会影响当前绑定的帧缓冲。我们也可以使用GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER,将一个帧缓冲分别绑定到读取目标或写入目标。绑定到GL_READ_FRAMEBUFFER的帧缓冲将会使用在所有像是glReadPixels的读取操作中,而绑定到GL_DRAW_FRAMEBUFFER的帧缓冲将会被用作渲染、清除等写入操作的目标。大部分情况都不需要区分它们,通常都会使用GL_FRAMEBUFFER。

删除缓冲区对象

在缓冲区对象不再被使用时,缓冲区对象可以通过调用glDeleteFramebuffers (GLsizei n, const GLuint* framebuffers)来删除。

glDeleteFramebuffers(1, &buffer);

和系统的帧缓冲区一样,帧缓冲区对象也包括颜色缓冲区、深度和模版缓冲区,这些逻辑上的缓冲区在帧缓冲区对象中称之为可附加的图像,它们是可以附加到帧缓冲区对象的二维像素数组。 FBO包含两种类型的附加图像:纹理图像(texture images)和渲染缓存图像(renderbuffer images)。如果纹理对象的图像数据关联到帧缓存,OpenGL执行的是“渲染到纹理”(render to texture)操作。如果渲染缓存的图像数据关联到帧缓存,OpenGL执行的是离屏渲染(offscreen rendering)。

渲染缓存区对象RenderBuffer(RBO)

渲染缓存是为离线渲染而新引进的。它允许将一个场景直接渲染到一个渲染缓存对象中,而不是渲染到纹理对象中。渲染缓存对象是用于存储单幅图像的数据存储区域。该图像按照一种可渲染的内部格式存储。它用于存储没有相关纹理格式的OpenGL逻辑缓存,比如模板缓存或者深度缓存。

一个renderbuffer对象是通过应用程序分配的一个2D图像缓冲区,renderbuffer可以用于分配和存储颜色,深度或者模板值,也可以作为一个framebuffer的颜色,深度,模板的附着,一个renderbuffer是一个类似于屏幕外的窗口系统提供的可绘制表面。但是renderbuffer不能直接用作GL纹理。

创建渲染缓冲区

 //1.定义一个缓存区ID
    GLuint buffer;
 //2.申请一个缓存区标志
    glGenRenderbuffers(1, &buffer);
//3.将标识符绑定到GL_RENDERBUFFER
    glBindRenderbuffer(GL_RENDERBUFFER, buffer);

和帧缓冲区对象一样,在引用渲染缓冲区对象之前必须绑定当前渲染缓冲对象,调用函数glBindRenderbuffer (GLenum target, GLuint renderbuffer)进行绑定, 第一个参数target是GL_RENDERBUFFER, 第二个参数是渲染缓冲区对象的ID。

删除缓冲区对象

在缓冲区对象不再被使用时,缓冲区对象可以通过调用glDeleteRenderbuffers (GLsizei n, const GLuint* renderbuffers)来删除。

glDeleteRenderbuffers (1, &buffer);

当一个渲染缓存被创建,它没有任何数据存储区域,所以还要为它分配空间,这可以通过用glRenderbufferStorage (GLenum target, GLenum internalformat, GLsizei width, GLsizei height)实现。

glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 600, 800);

第一个参数必须是GL_RENDERBUFFER, 第二个参数是可用于颜色,深度,模板的格式, width和height是渲染缓存图像的像素维度

附加渲染缓冲对象

最后,生成帧缓冲区之后,则需要将renderbuffer跟framebuffer进行绑定,调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用

//将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.myColorRenderBuffer);

下图展示了帧缓冲区对象,渲染缓冲区对象和纹理之间的关系,一个帧缓冲区对象中只能有一个颜色,深度,模板附着。

缓冲区关系.png
简单来说就是framebuffer只是一个管理者,其本身并没有任何存储区(没有存储纹理,顶点,颜色等数据),只是有颜色,深度,模板附着点,而真正存储这些数据的是renderbuffer。

GLSL渲染图片

这个案例大概实现以下这些内容: ·用EAGL 创建屏幕上的渲染表面 ·加载顶点/片元着⾊器 ·创建一个程序对象,并链接顶点/⽚元着⾊器,并链接程序对象 ·设置视口 ·清除颜色缓存区 ·渲染简单图元 ·使颜⾊缓存区的内容在EAGL 窗⼝表现呈现

#####1.创建顶点/片元着色器文件 着色器文件一般以.vsh/.fsh/.gsl为文件后缀名 顶点着色器shaderv.vsh

// 顶点坐标
attribute highp vec4 position;
// 纹理坐标
attribute highp vec2 textCoordinate;
// 纹理坐标
varying lowp vec2 varyTextCoord;
void main() {
    varyTextCoord = textCoordinate;
    gl_Position = position;
}

在着色器文件中最好不要加中文注释,以防编译无法通过,此处的中文注释只作为理解注释。

片元着色器shaderf.fsh

// 纹理坐标
varying lowp vec2 varyTextCoord;
// 纹理采样器(获取对应的纹理ID)
uniform sampler2D colorMap;
void main() {
    gl_FragColor = texture2D(colorMap, varyTextCoord);
}

创建一个UIView,并导入头文件#import <OpenGLES/ES2/gl.h>,此次用GLSL渲染图片的代码全部书写在这个UIView中。

//在iOS和tvOS上绘制OpenGL ES内容的图层,继承与CALayer
@property(nonatomic,strong)CAEAGLLayer *zhEagLayer;
@property(nonatomic,strong)EAGLContext *zhContext;
@property(nonatomic,assign)GLuint zhColorRenderBuffer;
@property(nonatomic,assign)GLuint zhColorFrameBuffer;
@property(nonatomic,assign)GLuint zhPrograme;
2.设置图层setupLayer
 //1.创建特殊图层
//这里需要重写layerClass,将ZHView返回的图层从CALayer替换成CAEAGLLayer
self.zhEagLayer = (CAEAGLLayer *)self.layer;
    
//2.设置scale
[self setContentScaleFactor:[[UIScreen mainScreen]scale]];
//3.设置描述属性,这里设置不维持渲染内容以及颜色格式为RGBA8
self.zhEagLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:@false,kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8,kEAGLDrawablePropertyColorFormat,nil];

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

重写layerClass

+(Class)layerClass
{
    return [CAEAGLLayer class];
}
3.设置渲染上下文setupContext
//1.指定OpenGL ES 渲染API版本
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
//2.创建图形上下文
EAGLContext *context = [[EAGLContext alloc]initWithAPI:api];
//3.判断是否创建成功
if (!context) {
        NSLog(@"Create failed!");
        return;
}
//4.设置图形上下文
if (![EAGLContext setCurrentContext:context]) {
        NSLog(@"Set failed!");
        return;
}
//5.将局部context赋值成全局的context
self.zhContext = context;
4.清空缓冲区deleteRenderAndFrameBuffer
//清空帧缓冲区
glDeleteBuffers(1, &_zhColorFrameBuffer);
self.zhColorFrameBuffer = 0;
//清空渲染缓冲区
glDeleteBuffers(1, &_zhColorRenderBuffer);
self.zhColorRenderBuffer = 0;

清空缓冲区的代码也可以写成:

glDeleteFramebuffers(1, &_zhColorFrameBuffer);
self.zhColorFrameBuffer = 0;

glDeleteRenderbuffers(1, &_zhColorRenderBuffer);
self.zhColorRenderBuffer = 0;
5.设置RenderBuffer
//1.定义一个缓存区ID
GLuint buffer;
//2.申请一个缓存区标志
glGenRenderbuffers(1, &buffer);
//3.将当前申请的buffer变成全局的
self.zhColorRenderBuffer = buffer;
//4.将标识符绑定到GL_RENDERBUFFER
glBindRenderbuffer(GL_RENDERBUFFER, self.zhColorRenderBuffer);
//5.将可绘制对象drawable object's  CAEAGLLayer的存储绑定到OpenGL ES renderBuffer对象
[self.zhContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.zhEagLayer];
6.设置FrameBuffer
//1.定义一个缓存区ID
GLuint buffer;
//2.申请一个缓存区标志
glGenRenderbuffers(1, &buffer);
//3.将buffer变成全局的
self.zhColorFrameBuffer = buffer;  
//4.绑定标识符到GL_FRAMEBUFFER
glBindFramebuffer(GL_FRAMEBUFFER, self.zhColorFrameBuffer);

/*生成帧缓存区之后,则需要将renderbuffer跟framebuffer进行绑定,
调用glFramebufferRenderbuffer函数进行绑定到对应的附着点上,后面的绘制才能起作用*/
    
//5.将渲染缓存区myColorRenderBuffer 通过glFramebufferRenderbuffer函数绑定到 GL_COLOR_ATTACHMENT0上。
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.zhColorRenderBuffer);
7.开始绘制renderLayer

1.设置清屏颜色

glClearColor(0.3f, 0.45f, 0.5f, 1.0f);

2.清除屏幕

glClear(GL_COLOR_BUFFER_BIT);

3.设置视口大小

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);

4.读取顶点着色程序、片元着色程序

NSString *vertFile = [[NSBundle mainBundle]pathForResource:@"shaderv" ofType:@"vsh"];
NSString *fragFile = [[NSBundle mainBundle]pathForResource:@"shaderf" ofType:@"fsh"];

5.加载着色器shader

self.zhPrograme = [self loadShaders:vertFile Withfrag:fragFile];

//加载shader
-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
    //1.定义2个临时的着色器对象
    GLuint verShader, fragShader;
    //创建program
    GLint program = glCreateProgram();
    //2.编译顶点着色程序和片元着色器程序
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    /*
    关于这个compileShader:type:file:方法传入的三个参数:
    参数1:编译完存储的底层地址
    参数2:编译的着色器的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
    参数3:文件路径
    */
    
    //3.链接着色器对象和程序对象
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    //4.释放不需要的shader
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}

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

6.链接程序对象

glLinkProgram(self.zhPrograme);
//检查链接是否成功
GLint linkStatus;
//获取链接状态
glGetProgramiv(self.myPrograme, GL_LINK_STATUS, &linkStatus);
 if (linkStatus == GL_FALSE) {
        GLchar message[512];
        glGetProgramInfoLog(self.zhPrograme, sizeof(message), 0, &message[0]);
        NSString *messageString = [NSString stringWithUTF8String:message];
        NSLog(@"program link error:%@",messageString);
        return;
  }
NSLog(@"program link success!");

7.使用程序对象

glUseProgram(self.zhPrograme);

8.设置顶点坐标和纹理坐标

//坐标数组
GLfloat attrArr[] =
    {
        1.0f, -1.0f, -1.0f,     1.0f, 0.0f,
        -1.0f, 1.0f, -1.0f,     0.0f, 1.0f,
        -1.0f, -1.0f, -1.0f,    0.0f, 0.0f,
        
        1.0f, 1.0f, -1.0f,      1.0f, 1.0f,
        -1.0f, 1.0f, -1.0f,     0.0f, 1.0f,
        1.0f, -1.0f, -1.0f,     1.0f, 0.0f,
    };
//每一行的前3位为顶点坐标,后两位为纹理坐标

9.处理顶点数据

//(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);

10.将顶点数据传入到顶点着色器对象

将顶点数据通过self.zhPrograme中的传递到顶点着色器程序的position,通过以下三个函数处理顶点数据 ·glGetAttribLocation,用来获取vertex attribute的入口。 ·告诉OpenGL ES,通过glEnableVertexAttribArray从buffer读取数据(打开通道) ·glVertexAttribPointer设置读取数据的方式

//第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
GLuint position = glGetAttribLocation(self.zhPrograme, "position");
//设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(position);
//设置读取方式
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, NULL);

glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr) 参数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

11.处理纹理数据

//第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
GLuint textCoor = glGetAttribLocation(self.zhPrograme, "textCoordinate");
//设置合适的格式从buffer里面读取数据
glEnableVertexAttribArray(textCoor);
//设置读取方式
glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (float *)NULL + 3);

12.加载纹理

//1、将 UIImage 转换为 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判断图片是否获取成功
if (!spriteImage) {
       NSLog(@"load image failed: %@", fileName);
       return;
   }
    
//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.使用默认方式绘制
CGContextTranslateCTM(spriteContext, 0, rect.size.height);
CGContextScaleCTM(spriteContext, 1.0, -1.0);
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, 0表示没有进行缩小的原始图片等级。
     参数3:纹理的颜色值GL_RGBA, 表示了纹理所采用的内部格式,内部格式是我们的像素数据在显卡中存储的格式,这里的GL_RGB显然就表示纹理中像素的颜色值是以RGB的格式存储的。
     参数4:纹理的宽
     参数5:纹理的高
     参数6:border,边界宽度,通常为0.
     参数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); 

13.设置纹理采样器 sampler2D

glUniform1i(glGetUniformLocation(self.zhPrograme, "colorMap"), 0);

14.绘图

glDrawArrays(GL_TRIANGLES, 0, 6);

15.从渲染缓冲区显示到屏幕上

[self.zhContext presentRenderbuffer:GL_RENDERBUFFER];

至此用GLSL渲染图片的代码已经基本完成,如果没有处理图片的翻转问题的话,运行的效果显示出来图片是翻转的,至于图片翻转的原因和解决图片翻转的方法已在上一篇文章中做了详细的介绍,这里就不再一一赘述。

下面将上述的代码步骤做一个梳理,如下图所示:

GLSL渲染图片.png

文中部分内容参考:《OpenGL ES 3.0 编程指南》