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

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

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

帧缓冲区对象FrameBuffer(FBO)

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

咱们知道,在应用程序调用任何的OpenGL ES命令以前,须要首先建立一个渲染上下文和绘图表面,并使之成为现行上下文和表面,以前在渲染的时候,其实一直使用的是原生窗口系统(好比EAGL,GLFW)提供的渲染上下文和绘图表面(即帧缓冲区)。 通常状况下,咱们只须要系统提供的帧缓冲区做为绘图表面,可是又有些特殊状况,好比阴影贴图、动态反射、处理后特效等须要渲染到纹理操做的,若是使用系统提供的帧缓冲区,效率会比较低低下,所以须要自定义本身的帧缓冲区。缓存

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

建立帧缓冲区对象框架

//定义一个缓存区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能够解绑定当前帧缓冲区对象。ide

在绑定到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.加载纹理

//一、将 UIImage 转换为 CGImageRef
CGImageRef spriteImage = [UIImage imageNamed:fileName].CGImage;
//判断图片是否获取成功
if (!spriteImage) {
       NSLog(@"load image failed: %@", fileName);
       return;
   }
    
//二、读取图片的大小,宽和高
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);
//五、在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);
//七、画图完毕就释放上下文
CGContextRelease(spriteContext);
//八、绑定纹理到默认的纹理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 编程指南》

相关文章
相关标签/搜索