本文主要介绍,如何使用 OpenGL ES 来渲染一张图片。内容包括:基础概念的讲解,如何使用 GLKit 来渲染纹理,如何使用 GLSL 编写的着色器来渲染纹理。html
OpenGL(Open Graphics Library) 是 Khronos Group (一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准)开发维护的一个规范,它是硬件无关的。它主要为咱们定义了用来操做图形和图片的一系列函数的 API,OpenGL 自己并不是 API。ios
OpenGL ES(OpenGL for Embedded Systems) 是 OpenGL 的子集,针对手机、PDA 和游戏主机等嵌入式设备而设计。该规范也是由 Khronos Group 开发维护。git
OpenGL ES 去除了四边形(GL_QUADS)、多边形(GL_POLYGONS) 等复杂图元,以及许多非绝对必要的特性,剩下最核心有用的部分。能够理解成是一个在移动平台上可以支持 OpenGL 最基本功能的精简规范。github
目前 iOS 平台支持的有 OpenGL ES 1.0,2.0,3.0。OpenGL ES 3.0 加入了一些新的特性,可是它除了须要 iOS 7.0 以上以外,还须要 iPhone 5S 以后的设备才能支持。出于现有设备的考虑,咱们主要使用 OpenGL ES 2.0。编程
注: 下文中的 OpenGL ES 均指代 OpenGL ES 2.0。小程序
OpenGL ES 部分运行在 CPU 上,部分运行在 GPU 上,为了协调这两部分的数据交换,定义了缓存(Buffers) 的概念。CPU 和 GPU 都有独自控制的内存区域,缓存能够避免数据在这两块内存区域之间进行复制,提升效率。缓存实际上就是指一块连续的 RAM 。数组
纹理是一个用来保存图像颜色的元素值的缓存,渲染是指将数据生成图像的过程。纹理渲染则是将保存在内存中的颜色值等数据,生成图像的过程。缓存
一、OpenGL ES 坐标系markdown
OpenGL ES 坐标系的范围是 -1 ~ 1,是一个三维的坐标系,一般用 X、Y、Z 来表示。Z 轴的正方向指向屏幕外。在不考虑 Z 轴的状况下,左下角为 (-1, -1, 0),右上角为 (1, 1, 0)。app
二、纹理坐标系
纹理坐标系的范围是 0 ~ 1,是一个二维坐标系,横轴称为 S 轴,纵轴称为 T 轴。在坐标系中,点的横坐标通常用 U 表示,点的纵坐标通常用 V 表示。左下角为 (0, 0),右上角为 (1, 1)。
注: UIKit 坐标系的 (0, 0) 点在左上角,其纵轴的方向和纹理坐标系纵轴的方向恰好相反。
注: (U, V) 可能会超出 0 ~ 1 这个范围,须要经过
glTextParameteri()
配置相应的方案,来映射到 S 轴和 T 轴。
在实际应用中,咱们须要使用各类各样的缓存。好比在纹理渲染以前,须要生成一块保存了图像数据的纹理缓存。下面介绍一下缓存管理的通常步骤:
使用缓存的过程能够分为 7 步:
glGenBuffers()
glBindBuffer()
glBufferData()
/ glBufferSubData()
glEnableVertexAttribArray()
/ glDisableVertexAttribArray()
glVertexAttribPointer()
glDrawArrays()
/ glDrawElements()
glDeleteBuffers()
这 7 步很重要,如今先有个印象,后面咱们在实际例子中会反复用到。
OpenGL ES 是一个状态机,相关的配置信息会被保存在一个上下文(Context) 中,这个些值会被一直保存,直到被修改。但咱们能够配置多个上下文,经过调用 [EAGLContext setCurrentContext:context]
来切换。
图元(Primitive) 是指 OpenGL ES 中支持渲染的基本图形。OpenGL ES 只支持三种图元,分别是顶点、线段、三角形。复杂的图形得经过渲染多个三角形来实现。
渲染三角形的基本流程按照上图所示。其中,顶点着色器和片断着色器是可编程的部分,着色器(Shader) 是一个小程序,它们运行在 GPU 上,在主程序运行的时候进行动态编译,而不用写死在代码里面。编写着色器用的语言是 GLSL(OpenGL Shading Language) ,在第三节中咱们会详细介绍。
下面介绍一下渲染流程的每一步都作了什么:
一、顶点数据
为了渲染一个三角形,咱们须要传入一个包含 3 个三维顶点坐标的数组,每一个顶点都有对应的顶点属性,顶点属性中能够包含任何咱们想用的数据。在上图的例子里,咱们的每一个顶点包含了一个颜色值。
而且,为了让 OpenGL ES 知道咱们是要绘制三角形,而不是点或者线段,咱们在调用绘制指令的时候,都会把图元信息传递给 OpenGL ES 。
二、顶点着色器
顶点着色器会对每一个顶点执行一次运算,它可使用顶点数据来计算该顶点的坐标、颜色、光照、纹理坐标等。
顶点着色器的一个重要任务是进行坐标转换,例如将模型的原始坐标系(通常是指其 3D 建模工具中的坐标)转换到屏幕坐标系。
三、图元装配
在顶点着色器程序输出顶点坐标以后,各个顶点按照绘制命令中的图元类型参数,以及顶点索引数组被组装成一个个图元。
经过这一步,模型中 3D 的图元已经被转化为屏幕上 2D 的图元。
四、几何着色器
在「OpenGL」的版本中,顶点着色器和片断着色器之间有一个可选的着色器,叫作几何着色器(Geometry Shader)。
几何着色器把图元形式的一系列顶点的集合做为输入,它能够经过产生新顶点构造出新的图元来生成其余形状。
OpenGL ES 目前还不支持几何着色器,这个部分咱们能够先不关注。
五、光栅化
在光栅化阶段,基本图元被转换为供片断着色器使用的片断。片断表示能够被渲染到屏幕上的像素,它包含位置、颜色、纹理坐标等信息,这些值是由图元的顶点信息进行插值计算获得的。
在片断着色器运行以前会执行裁切,处于视图之外的全部像素会被裁切掉,用来提高执行效率。
六、片断着色器
片断着色器的主要做用是计算每个片断最终的颜色值(或者丢弃该片断)。片断着色器决定了最终屏幕上每个像素点的颜色值。
七、测试与混合
在这一步,OpenGL ES 会根据片断是否被遮挡、视图上是否已存在绘制好的片断等状况,对片断进行丢弃或着混合,最终被保留下来的片断会被写入帧缓存中,最终呈如今设备屏幕上。
因为 OpenGL ES 只能渲染三角形,所以多边形须要由多个三角形来组成。
如图所示,一个五边形,咱们能够把它拆分红 3 个三角形来渲染。
渲染一个三角形,咱们须要一个保存 3 个顶点的数组。这意味着咱们渲染一个五边形,须要用 9 个顶点。并且咱们能够看到,其中 V0 、 V2 、V3 都是重复的顶点,显得有点冗余。
那么有没有更简单的方式,可让咱们复用以前的顶点呢?答案是确定的。
在 OpenGL ES 中,对于三角形有 3 种绘制模式。在给定的顶点数组相同的状况下,能够指定咱们想要的链接方式。以下图所示:
一、GL_TRIANGLES
GL_TRIANGLES
就是咱们一开始说的方式,没有复用顶点,以每三个顶点绘制一个三角形。第一个三角形使用 V0 、 V1 、V2 ,第二个使用 V3 、 V4 、V5 ,以此类推。若是顶点的个数不是 3 的倍数,那么最后的 1 个或者 2 个顶点会被舍弃。
二、GL_TRIANGLE_STRIP
GL_TRIANGLE_STRIP
在绘制三角形的时候,会复用前两个顶点。第一个三角形依然使用 V0 、 V1 、V2 ,第二个则会使用 V1 、 V2 、V3,以此类推。第 n 个会使用 V(n-1) 、 V(n) 、V(n+1) 。
三、GL_TRIANGLE_FAN
GL_TRIANGLE_FAN
在绘制三角形的时候,会复用第一个顶点和前一个顶点。第一个三角形依然使用 V0 、 V1 、V2 ,第二个则会使用 V0 、 V2 、V3,以此类推。第 n 个会使用 V0 、 V(n) 、V(n+1) 。这种方式看上去像是在绕着 V0 画扇形。
恭喜你终于看完了枯燥的概念讲解。从这里开始,咱们开始会进入实际的例子,用代码来说解渲染的过程。
在 GLKit 中,苹果爸爸对 OpenGL ES 中的一些操做进行了封装,所以咱们使用 GLKit 来渲染会省去一些步骤。
那么好奇的你确定会问,在「纹理渲染」这件事情上,GLKit 帮咱们作了什么呢?
先不着急,等咱们讲完第三节中使用 GLSL 渲染的方式,再来回答这个问题。
如今,让咱们怀着忐忑又期待的心情,来看看 GLKit 是怎么渲染纹理的。
定义顶点数据,用一个三维向量来保存 (X, Y, Z) 坐标,用一个二维向量来保存 (U, V) 坐标:
typedef struct { GLKVector3 positionCoord; // (X, Y, Z) GLKVector2 textureCoord; // (U, V) } SenceVertex; 复制代码
初始化顶点数据:
self.vertices = malloc(sizeof(SenceVertex) * 4); // 4 个顶点 self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}}; // 左上角 self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}}; // 左下角 self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}}; // 右上角 self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}}; // 右下角 复制代码
退出的时候,记得手动释放内存:
- (void)dealloc { // other code ... if (_vertices) { free(_vertices); _vertices = nil; } } 复制代码
// 建立上下文,使用 2.0 版本 EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; // 初始化 GLKView CGRect frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width); self.glkView = [[GLKView alloc] initWithFrame:frame context:context]; self.glkView.backgroundColor = [UIColor clearColor]; self.glkView.delegate = self; [self.view addSubview:self.glkView]; // 设置 glkView 的上下文为当前上下文 [EAGLContext setCurrentContext:self.glkView.context]; 复制代码
使用 GLKTextureLoader
来加载纹理,并用 GLKBaseEffect
保存纹理的 ID ,为后面渲染作准备。
NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"sample.jpg"]; UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)}; GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage] options:options error:NULL]; self.baseEffect = [[GLKBaseEffect alloc] init]; self.baseEffect.texture2d0.name = textureInfo.name; self.baseEffect.texture2d0.target = textureInfo.target; 复制代码
由于纹理坐标系和 UIKit 坐标系的纵轴方向是相反的,因此将 GLKTextureLoaderOriginBottomLeft
设置为 YES
,用来消除两个坐标系之间的差别。
注: 这里若是用
imageNamed:
来读取图片,在反复加载相同纹理的时候,会出现上下颠倒的错误。
在 glkView:drawInRect:
代理方法中,咱们要去实现顶点数据和纹理数据的绘制逻辑。这一步是重点,注意观察「缓存管理的 7 个步骤」的具体用法。
代码以下:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { [self.baseEffect prepareToDraw]; // 建立顶点缓存 GLuint vertexBuffer; glGenBuffers(1, &vertexBuffer); // 步骤一:生成 glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); // 步骤二:绑定 GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4; glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW); // 步骤三:缓存数据 // 设置顶点数据 glEnableVertexAttribArray(GLKVertexAttribPosition); // 步骤四:启用或禁用 glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord)); // 步骤五:设置指针 // 设置纹理数据 glEnableVertexAttribArray(GLKVertexAttribTexCoord0); // 步骤四:启用或禁用 glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord)); // 步骤五:设置指针 // 开始绘制 glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 步骤六:绘图 // 删除顶点缓存 glDeleteBuffers(1, &vertexBuffer); // 步骤七:删除 vertexBuffer = 0; } 复制代码
咱们调用 GLKView
的 display
方法,便可以触发 glkView:drawInRect:
回调,开始渲染的逻辑。
代码以下:
[self.glkView display]; 复制代码
至此,使用 GLKit 实现纹理渲染的过程就介绍完毕了。
是否是以为意犹未尽,那就赶快进入下一节,了解如何直接经过 GLSL 编写的着色器来渲染纹理。
在这一小节,咱们会讲解在不使用 GLKit 的状况下,怎么实现纹理渲染。咱们会着重介绍与 GLKit 渲染不一样的部分。
注: 你们实际去查看 demo 的时候,会发现仍是有引入
<GLKit/GLKit.h>
这个头文件。这里主要是为了使用GLKVector3
、GLKVector2
这两个类型,固然不使用也是彻底能够的。目的是为了和 GLKit 的例子保持数据格式的一致,方便你们把注意力放在二者真正差别的部分。
首先,咱们须要本身编写着色器,包括顶点着色器和片断着色器,使用的语言是 GLSL 。这里对于 GLSL 就不展开讲了,只解释一下咱们等下会用到的部分,更详细的语法内容,能够参见 这里。
新建一个文件,通常顶点着色器用后缀 .vsh
,片断着色器用后缀 .fsh
(固然你不喜欢这么命名也能够,可是为了方便其余人阅读,最好是仍是按照这个规范来),而后就能够写代码了。
顶点着色器的代码以下:
attribute vec4 Position; attribute vec2 TextureCoords; varying vec2 TextureCoordsVarying; void main (void) { gl_Position = Position; TextureCoordsVarying = TextureCoords; } 复制代码
片断着色器的代码以下:
precision mediump float; uniform sampler2D Texture; varying vec2 TextureCoordsVarying; void main (void) { vec4 mask = texture2D(Texture, TextureCoordsVarying); gl_FragColor = vec4(mask.rgb, 1.0); } 复制代码
GLSL 是类 C 语言写成,若是学习过 C 语言,上手是很快的。下面对这两个着色器的代码作一下简单的解释。
attribute
修饰符只存在于顶点着色器中,用于储存每一个顶点信息的输入,好比这里定义了 Position
和 TextureCoords
,用于接收顶点的位置和纹理信息。
vec4
和 vec2
是数据类型,分别指四维向量和二维向量。
varying
修饰符指顶点着色器的输出,同时也是片断着色器的输入,要求顶点着色器和片断着色器中都同时声明,并彻底一致,则在片断着色器中能够获取到顶点着色器中的数据。
gl_Position
和 gl_FragColor
是内置变量,对这两个变量赋值,能够理解为向屏幕输出片断的位置信息和颜色信息。
precision
能够为数据类型指定默认精度,precision mediump float
这一句的意思是将 float
类型的默认精度设置为 mediump
。
uniform
用来保存传递进来的只读值,该值在顶点着色器和片断着色器中都不会被修改。顶点着色器和片断着色器共享了 uniform
变量的命名空间,uniform
变量在全局区声明,同个 uniform
变量在顶点着色器和片断着色器中都能访问到。
sampler2D
是纹理句柄类型,保存传递进来的纹理。
texture2D()
方法能够根据纹理坐标,获取对应的颜色信息。
那么这两段代码的含义就很明确了,顶点着色器将输入的顶点坐标信息直接输出,并将纹理坐标信息传递给片断着色器;片断着色器根据纹理坐标,获取到每一个片断的颜色信息,输出到屏幕。
少了 GLKTextureLoader
的相助,咱们就只能本身去生成纹理了。生成纹理的步骤比较固定,如下封装成一个方法:
- (GLuint)createTextureWithImage:(UIImage *)image { // 将 UIImage 转换为 CGImageRef CGImageRef cgImageRef = [image CGImage]; GLuint width = (GLuint)CGImageGetWidth(cgImageRef); GLuint height = (GLuint)CGImageGetHeight(cgImageRef); CGRect rect = CGRectMake(0, 0, width, height); // 绘制图片 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); void *imageData = malloc(width * height * 4); CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextTranslateCTM(context, 0, height); CGContextScaleCTM(context, 1.0f, -1.0f); CGColorSpaceRelease(colorSpace); CGContextClearRect(context, rect); CGContextDrawImage(context, rect, cgImageRef); // 生成纹理 GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); // 将图片数据写入纹理缓存 // 设置如何把纹素映射成像素 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 解绑 glBindTexture(GL_TEXTURE_2D, 0); // 释放内存 CGContextRelease(context); free(imageData); return textureID; } 复制代码
对于写好的着色器,须要咱们在程序运行的时候,动态地去编译连接。编译一个着色器的代码也比较固定,这里经过后缀名来区分着色器类型,直接看代码:
- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType { // 查找 shader 文件 NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"]; // 根据不一样的类型肯定后缀名 NSError *error; NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error]; if (!shaderString) { NSAssert(NO, @"读取shader失败"); exit(1); } // 建立一个 shader 对象 GLuint shader = glCreateShader(shaderType); // 获取 shader 的内容 const char *shaderStringUTF8 = [shaderString UTF8String]; int shaderStringLength = (int)[shaderString length]; glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength); // 编译shader glCompileShader(shader); // 查询 shader 是否编译成功 GLint compileSuccess; glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess); if (compileSuccess == GL_FALSE) { GLchar messages[256]; glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]); NSString *messageString = [NSString stringWithUTF8String:messages]; NSAssert(NO, @"shader编译失败:%@", messageString); exit(1); } return shader; } 复制代码
顶点着色器和片断着色器一样都须要通过这个编译的过程,编译完成后,还须要生成一个着色器程序,将这两个着色器连接起来,代码以下:
- (GLuint)programWithShaderName:(NSString *)shaderName { // 编译两个着色器 GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER]; GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER]; // 挂载 shader 到 program 上 GLuint program = glCreateProgram(); glAttachShader(program, vertexShader); glAttachShader(program, fragmentShader); // 连接 program glLinkProgram(program); // 检查连接是否成功 GLint linkSuccess; glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess); if (linkSuccess == GL_FALSE) { GLchar messages[256]; glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]); NSString *messageString = [NSString stringWithUTF8String:messages]; NSAssert(NO, @"program连接失败:%@", messageString); exit(1); } return program; } 复制代码
这样,咱们只要将两个着色器命名统一,按照规范添加后缀名。而后将着色器名称传入这个方法,就能够得到一个编译连接好的着色器程序。
有了着色器程序后,咱们就须要往程序中传入数据,首先要获取着色器中定义的变量,具体操做以下:
注: 不一样类型的变量获取方式不一样。
GLuint positionSlot = glGetAttribLocation(program, "Position"); GLuint textureSlot = glGetUniformLocation(program, "Texture"); GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords"); 复制代码
传入生成的纹理 ID:
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureID); glUniform1i(textureSlot, 0); 复制代码
glUniform1i(textureSlot, 0)
的意思是,将 textureSlot
赋值为 0
,而 0
与 GL_TEXTURE0
对应,这里若是写 1
,glActiveTexture
也要传入 GL_TEXTURE1
才能对应起来。
设置顶点数据:
glEnableVertexAttribArray(positionSlot); glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord)); 复制代码
设置纹理数据:
glEnableVertexAttribArray(textureCoordsSlot); glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord)); 复制代码
在渲染纹理的时候,咱们须要指定 Viewport 的尺寸,能够理解为渲染的窗口大小。调用 glViewport
方法来设置:
glViewport(0, 0, self.drawableWidth, self.drawableHeight); 复制代码
// 获取渲染缓存宽度 - (GLint)drawableWidth { GLint backingWidth; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); return backingWidth; } // 获取渲染缓存高度 - (GLint)drawableHeight { GLint backingHeight; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); return backingHeight; } 复制代码
经过以上步骤,咱们已经拥有了纹理,以及顶点的位置信息。如今到了最后一步,咱们要怎么将缓存与视图关联起来?换句话说,假如屏幕上有两个视图,OpenGL ES 要怎么知道将图像渲染到哪一个视图上?
因此咱们要进行渲染层绑定。经过 renderbufferStorage:fromDrawable:
来实现:
- (void)bindRenderLayer:(CALayer <EAGLDrawable> *)layer { GLuint renderBuffer; // 渲染缓存 GLuint frameBuffer; // 帧缓存 // 绑定渲染缓存要输出的 layer glGenRenderbuffers(1, &renderBuffer); glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer); [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]; // 将渲染缓存绑定到帧缓存上 glGenFramebuffers(1, &frameBuffer); glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBuffer); } 复制代码
以上代码生成了一个帧缓存和一个渲染缓存,并将渲染缓存挂载到帧缓存上,而后设置渲染缓存的输出层为 layer
。
最后,将绑定的渲染缓存呈现到屏幕上:
[self.context presentRenderbuffer:GL_RENDERBUFFER]; 复制代码
至此,使用 GLSL 渲染纹理的关键步骤就结束了。
最终效果:
综上所述,咱们能够回答第二节的问题了,GLKit 主要帮咱们作了如下几个点:
GLKTextureLoader
封装了一个将 Image 转化为 Texture 的方法。GLKBaseEffect
内部实现了着色器的编译连接过程,咱们在使用过程当中基本能够忽略「着色器」这个概念。GLKView
在调用 display
方法的时候,会在内部去设置。GLKView
内部会调用 renderbufferStorage:fromDrawable:
将自身的 layer
设置为渲染缓存的输出层。所以,在调用 display
方法的时候,内部会调用 presentRenderbuffer:
去将渲染缓存呈现到屏幕上。请到 GitHub 上查看完整代码。
获取更佳的阅读体验,请访问原文地址 【Lyman's Blog】从零讲解 iOS 中 OpenGL ES 的纹理渲染