OpenGL ES入门第一篇-OpenGL ES初探

6,244 阅读25分钟

OpenGL ES 简介

      OpenGL ES (OpenGL for Embedded Systems) 是以手持和嵌入式为目标的⾼级3D图形应 ⽤程序编程接口(API). OpenGL ES 是⽬前智能手机中占据统治地位的图形API.⽀持的平台: iOS, Andriod , BlackBerry ,bada ,Linux ,Windows.

     另外,作为一名iOS开发者我们当然要看看Apple 官方文档关于OpenGL ES 的解释。

The Open Graphics Library (OpenGL) is used for visualizing 2D and 3D data. It is a multipurpose open- standard graphics library that supports applications for 2D and 3D digital content creation, mechanical and architectural design, virtual prototyping, flight simulation, video games, and more. You use OpenGL to configure a 3D graphics pipeline and submit data to it. Vertices are transformed and lit, assembled into primitives, and rasterized to create a 2D image. OpenGL is designed to translate function calls into graphics commands that can be sent to underlying graphics hardware. Because this underlying hardware is dedicated to processing graphics commands, OpenGL drawing is typically very fast. OpenGL for Embedded Systems (OpenGL ES) is a simplified version of OpenGL that eliminates redundant functionality to provide a library that is both easier to learn and easier to implement in mobile graphics hardware

     开放式图形库(OpenGL)⽤于可视化的⼆维和三维数据。它是一个多功能开放式标准图形库,⽀持2D和3D数字内容创建,机械和建筑设计,虚拟原型设计,⻜行模拟,视频游戏等应⽤用程序。您可以使用OpenGL配置3D图形管道并向其提交数据。顶点被变换和点亮,组合成图元,并光栅化以创建2D图像。OpenGL旨在将函数调⽤转换为可以发送到底层图形硬件的图形命令。由于此底层硬件专用于处理图形命令,因此OpenGL绘图通常⾮常快。

     OpenGL for Embedded Systems(OpenGL ES)是OpenGL的简化版本,它消除了冗余功能,提供了⼀个既易于学习⼜更易于在移动图形硬件中实现的库。

     由于OpenGL ES 是OpenGL 的简化版,因此在学习OpenGL ES前有必要去先了解一下基本的OpenGL 的知识,因此推荐大家先去看看前面关于Open GL 的文章

OpenGL ES 渲染流程解析


      如上图所示,整个渲染流程应该是

  1. 通过客户端将图元数据(顶点坐标,纹理坐标,变换矩阵等等)和图片数据(纹理)通过attribute 或者 uniform等通道传入到顶点着色器
  2. 顶点着色器利用传入的顶点坐标和变换矩阵做各种变换,生成最终的顶点位置,如果有光照还可以通过光照计算公式生成逐顶点光照颜色
  3. 需要注意的是顶点着色器之后并不是马上就进入了片元着色器,而是先经过了图元装配;在这个阶段会执行裁剪、透视分割和 Viewport变换等操作。然后再经过光栅化才到片元着色器
  4.  光栅化就是将图元转化成一组二维片段的过程.而这些转化的片段将由片元着⾊器处理.这些⼆维片段就是屏幕上可绘制的像素. 而片元着色器的任务就是给这些像素填充颜色,可以是通过attribute通道传入的颜色,也可以是通过纹理图片提取的纹素(某个纹理坐标对应的纹理颜色);还可以是自己计算的颜色(比如混合等等)
  5. 最后到达帧缓冲区,进行透明度、模板、深度测试、混合等等。注意这里的混合和片元着色器里的混合有点不一样,是指将新生成的⽚段颜⾊与保存在帧缓存区相应位置的颜色值组合起来.


EGL与EAGL

       OpenGL ES 命令需要渲染上下文和绘图表面才能完成图形图像的绘制。其中渲染上下文用来存储相关OpenGL ES的状态;绘制表面则是用来绘制图元的表面,它指定渲染所需要的缓存区类型,比如颜色缓冲区深度缓冲区和模板缓冲区。

       然而OpenGL ES 并没有提供如何创建渲染上下文或者上下文如何连接到原生窗口系统的API。而EGL是Khronos渲染API【如OpenGL ES】和原生窗口系统之间的接口。因为每个窗⼝系统都有不同的定义,所以EGL提供基本的不透明类型—EGLDisplay, 这个类型封装了所有系统相关性,用于和原生窗口系统的接口.但是我们iOS平台是唯一支持OpenGL ES 缺不支持EGL的平台。Apple提供了自己的EGL API实现,称为EAGL。

       由于OpenGL ES 是基于C的API,因此非常方便且受到广泛支持。作为C API,它与Objective-C Cocoa Touch 应用程序无缝集成。但是,OpenGL ES 规范并没有定义窗口层;相反,托管操作系统必须提供函数来创建一个接受命令的OpenGL ES 渲染上下文和一个帧缓冲区,其中写入任何绘图命令的结果。因此在iOS上使⽤OpenGL ES需要使用iOS类来设置和呈现绘图表面,并使⽤平台中相应的API来呈现其内容。

EAGLContext 

     EAGLContext是苹果自己为Open GL 提供的渲染上下文,该对象管理着利用OpenGL 渲染图形所需要的状态信息、命令、资源等等。在IOS应用程序中,每个线程都会维护一个当前上下文。当应用程序调用Opengl ES的API时,线程的上下文就会被那个API改变(改变其管理的状态、命令、资源等等)。

      要设置当前上下文,可以通过调用EAGLContext类的setCurrentContext:方法来实现,当然也可以通过EAGLContext类的currentContext方法来获取一个线程的当前上下文。另外,在创建和初始化EAGLContext对象时,可以选择使用哪个版本的Opengl ES   API。创建Opengl ES 3.0上下文时,如下初始化:

EAGLContext* myContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];

      如果设备不支持您锁设置的OpenGL ES API,那么 initWithAPI:方法将返回nil。为了支持OpenGL ES API的多个版本,应该首先尝试初始化为最新版本的渲染上下文。如果返回的对象为nil,则初始化旧版本的上下文。如:

EAGLContext* CreateBestEAGLContext()
{
   EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
   if (context == nil) {
      context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   }
   return context;
}

     上下文的API属性指出了上下文支持的OpenGL ES版本。我们应该首先测试上下文的API属性然后使用它来选择正确的渲染路径。实现此行为的常见模式是为每个渲染路径创建一个类。应用程序在初始化时测试上下文并创建一次渲染器。

EAGLSharegroup

      尽管上文说到EAGLContext管理着 OpenGL 的状态,但其实OpenGL的状态并不是直接由EAGLContext来管理的,而是EAGLContext 借助EAGLContext来管理的。换句话说,OpenGL ES的状态由EAGLSharegroup对象来创建和维护;每个EAGLContext都包含一个EAGLSharegroup对象,它将OpenGL的状态维护委托给EAGLSharegroup。3者的关系如下图:


       大家都知道在移动端资源是非常匮乏的,在多个上下文中创建同样内容的备份是非常奢侈的操作;如果能够共享通用的资源则可以更好的利用设备的图形资源。这个时候EAGLSharegroup将变得非常有用,当多个上下文关联到同一个EAGLSharegroup时,被任意一个上下文创建的Opengl ES对象在所有的上下文中都是可用的。注意:共享同一个共享组的所有上下文,都必须使用同一个版本的Opengl ES API来初始化上下文

       当共享组是被多个上下文共享时,我们就有义务要管理Opengl ES对象状态的改变。规则如下:

  1. 当应用程序可能要通过多个上下文进入某个对象的同时,要确保对象没有被同时改变。
  2. 当对象要被发给上下文的命令改变时,对象此时不能被另外的上下文读取或者改变。
  3. 在一个对象被改变时,必须是被绑定对象所有的上下文才能看到这些改变。如果一个上下文在绑定之前就引用它,那么这个对象的内容是没有被定义的

 GLKit框架概述  

       GLKit 框架的设计目标是为了简化基于OpenGL / OpenGL ES的应用开发。它的出现加快了OpenGL / OpenGL ES 应用的开发。GLKit使用数学库,背景纹理加载,预先创建的着色器效果,以及标准视图和视图控制器来实现渲染循环。

     GLKit框架提供了功能和类,可以减少创建新的基于着⾊器的应⽤程序所需的⼯作量,或者⽀持依赖早期版本的OpenGL ES或OpenGL提供的固定管线功能来处理应用程序。

      GLKView 提供绘制场所(View);GLKViewController(扩展于标准的UIKit. ⽤于绘制视图内容的管理与呈现.) 

     另外需要注意的是虽然苹果现在已经弃⽤了OpenGL ES(底层渲染从OpenGL ES改为metal),但由于OpenGL ES 是基于C的API,作为C API,它与Objective-C Cocoa Touch 应用程序无缝集成,因此iOS开发者可以继续使⽤,因此在使用GLKit相关API时虽然会有相应警告但是并不影响渲染结果。 

GLKit渲染图形的基本流程

     使用GLKit渲染图形时首先需要创建一个GLKView作为绘制表面,同时设置其与OpenGL ES 相关基本参数,比如配置渲染缓存区的格式等等;接着我们需要按照需求调用GLKit相关API设置相关图形参数(比如颜色、纹理、顶点坐标等等)来完成相关渲染,最后将渲染结果呈现在手机屏幕上。文字描述可能有点抽象,具体如下图所示:

                     

        上图中可以看到一个frameBuffer频繁出现,那么什么是frameBuffer(帧缓冲区)呢,其实帧缓冲区是由像素组成的二维数组,每一个存储单元对应屏幕上的一个像素,整个帧缓冲对应一帧图像即当前屏幕画面。帧缓冲通常包括:颜色缓冲,深度缓冲,模板缓冲和累积缓冲。这些缓冲区可能是在一块内存区域,也可能单独分开,看硬件。而像素数据在进入帧缓冲之前(称为片元)必须通过一系列测试才能写入帧缓冲,如果片元在其中某个测试没有通过,后面的测试或操作都将不再进行。这些测试或操作流程是:开始(片元)-裁剪测试-alpha测试-模板测试-深度测试-混合-抖动-逻辑操作-结束(写入帧缓冲),这一系列操作都是针对片元着色器的输出(片元的),所以又称之为逐片元操作。

     帧缓冲区可以简单的理解为存储绘制结果的地方;了解了什么是帧缓冲区后,那么理解上面的图就不难了, 首先需要设置一下帧缓冲区的格式,包括颜色缓冲、深度缓冲等等的格式,不同的格式绘制出的图形视觉效果可能不一样;其次就可以用代码来具体实现绘制操作了,绘制的结果会暂时存储在帧缓冲区;最后等一幅图像绘制完毕后就可以交由设备屏幕呈现了。

GLKit相关API介绍

      这里我们只介绍一些常见的类和API,基本能满足大家90%以上的开发任务了,对于个别比较冷门用处比较小的API等大家有相关需求的时候再去翻看相关API吧

  1. GLKTextureInfo  OpenGL纹理信息的抽象,里面封装了纹理相关的各种信息

           name : OpenGL上下⽂中到的纹理名称;
           target:纹理绑定的目标(表明纹理是几维的);
           height: 纹理的高度
           width: 纹理的高度
           textureOrigin: 纹理原点的位置
            alphaState: 纹理中alpha分量状态
            containsMipmaps: 布尔值,纹理是否包含mip贴图 

  2. GLKTextureLoader 纹理加载的工具类,能够简化从各种资源文件中加载纹理 

           - initWithSharegroup: 初始化方法,这里需要注意sharegroup这个参数是一个                    EAGLSharegroup类型的参数,sharegroup 对象管理与一个或多个EAGLContext对象关联的      OpenGL ES资源,若不指定或值为NULL则创建新的对象,当资源需要被共享时再使用它。

           + textureWithContentsOfFile:options:errer: 从文件中加载2D纹理图像数据,并利用      数据生成新的纹理 

           - textureWithContentsOfFile:options:queue:completionHandler: 从文件中异步
    加载2D纹理图像数据,并利用这些数据创建新纹理对象 

          - textureWithContentsOfURL:options:error: 从URL中加载2D纹理图像数据,并利            用数据创建新的纹理对象 
          - textureWithContentsOfURL:options:queue:completionHandler: 从URL中异步加        载2D纹理图像数据,并利用数据创建新的纹理对象 
         + textureWithContentsOfData:options:errer: 从内存中加载2D纹理图像数据,并根
    据数据创建新纹理 
        - textureWithContentsOfData:options:queue:completionHandler: 从内存中异步加        载2D纹理图像数据,并根据数据创建新纹理 
       - textureWithCGImage:options:error: 从Quartz图像加载2D纹理图像数据并利用数据创
    建新纹理对象
       - textureWithCGImage:options:queue:completionHandler: 从Quartz图像异步加载2D      纹理图像数据并利用数据创建新纹理对象
      + cabeMapWithContentsOfURL:options:errer: 从单个URL加载立方体贴图纹理
    图像数据,并根据数据创建新纹理
      - cabeMapWithContentsOfURL:options:queue:completionHandler: 从单个URL异步        加载立方体贴图纹理图像数据,并根据数据创建新纹理
      + cubeMapWithContentsOfFile:options:errer: 从单个文件加载立方体贴图纹理
    图像数据,并从数据中创建新纹理 

      - cubeMapWithContentsOfFile:options:queue:completionHandler: 从单个文件异步        加载立方体贴图纹理图像数据,并从数据中创建新纹理

      + cubeMapWithContentsOfFiles:options:errer: 从一系列⽂件中加载⽴方体贴图纹理图      像数据,并从数据创建新纹理 

      - cubeMapWithContentsOfFiles:options:options:queue:completionHandler:从一          系列⽂件中异步加载⽴方体贴图纹理图像数据,并从数据中创建新纹理 

3、GLKView 使用OpenGL ES 绘制内容的视图默认实现 

      - initWithFrame:context: 初始化新视图 
      delegate 视图代理
      drawableColorFormat 颜⾊渲染缓冲区格式
      drawableDepthFormat 深度渲染缓冲区格式 
      drawableStencilFormat 模板渲染缓冲区的格式
      drawableMultisample 多重采样缓冲区的格式 
      drawableHeight 底层渲染缓冲区对象的高度(以像素为单位)
      drawableWidth 底层渲染缓冲区对象的宽度(以像素为单位) 
      context 绘制视图内容时使⽤的OpenGL ES上下文
      - bindDrawable 将底层FrameBuffer 对象绑定到OpenGL ES
      enableSetNeedsDisplay 布尔值,控制setNeedsDisplay是否有效
      - display ⽴即重绘视图内容 

      snapshot 绘制视图内容并将其作为新图像对象(UIImage *)返回,这个方法永远不要在               draw 方法里调用,否则会递归死循环

     - deleteDrawable 删除与视图关联的可绘制对象 

4、GLKViewDelegate  GLKView对象的回调接口 

     - glkView:drawInRect: 绘制视图内容 (必须实现代理方法) 

5、GLKViewController 管理OpenGL ES渲染循环的视图控制器

     paused 布尔值,渲染循环是否已暂停
     pausedOnWillResignActive 布尔值,当前程序即将推出活动状态时视图控制器是否自动暂    停渲染循环
     resumeOnDidBecomeActive 布尔值,当前程序变为活动状态时视图控制是否自动恢复渲染循环 
     framesDisplayed 视图控制器自创建以来发送的帧更新数(即绘制了多少帧)
    timeSinceFirstResume 自视图控制器第⼀次恢复发送更新事件以来经过的时间量
    timeSinceLastResume ⾃上次视图控制器恢复发送更新事件以来经过的时间量
    timeSinceLastUpdate 自上次视图控制器调用委托⽅法(glkViewControllerUpdate:)后经过的时间量
    timeSinceLastDraw 自上次视图控制器调用视图display方法以来经过的时间量.
    preferredFramesPerSecond 视图控制器调用视图以及更新视图内容的速率(理想情况下的每秒的更新帧数) 

    framesPerSencond 视图控制器调用视图以及更新其内容的实际速率(这是个只读属性,因为实际帧率不会完全由开发者通过preferredFramesPerSecond设置的值来决定,还受到屏幕刷新率等等的影响,因此framesPerSencond只能在不超过屏幕刷新率的基础上尽量接近preferredFramesPerSecond) 。

6、GLKViewControllerDelegate 渲染循环回调⽅法

  - glkViewControllerUpdate: 在显示每个帧之前调用 
  - glkViewController:willPause: 在渲染循环暂停或恢复之前调用. 

7、GLKBaseEffect  一种简单光照/着⾊系统,⽤于基于着⾊器的OpenGL渲染 

   label 给Effect(效果)命名
   transform 绑定效果时应⽤于顶点数据的模型视图,投影和纹理变换 
   lightingType ⽤于计算每个⽚段的光照策略,值为GLKLightingType枚举(GLKLightingTypePerVertex 表示在三⻆形中每个顶点执行光照计算,然后在三角形⾏插值;GLKLightingTypePerPixel 表示光照计算的输入在三⻆形内插入,并且在每个⽚段执行光照计算) 简单点说就是这两个枚举值一个表示的是在顶点着色器计算光照,一个在片段着色器计算光照。
   lightModelTwoSided 布尔值,表示为图元的两侧计算光照
   material 计算光照时使用的材质属性
   lightModelAmbientColor 环境颜色,应⽤在所渲染的所有图元. 
   light0 场景中第⼀个光照属性 
   light1 场景中第二个光照属性 
   light2 场景中第三个光照属性 
   texture2d0 第一个纹理属性 
   texture2d1 第⼆个纹理属性 

   textureOrder 纹理应用于渲染图元的顺序
   colorMaterialEnable 布尔值,表示计算光照与材质交互时是否使用顶点颜色属性 
   useConstantColor 布尔值,指示是否使用常量颜⾊
   constantColor 不提供每个顶点颜色数据时使用的常量颜⾊
   - prepareToDraw 准备渲染效果 

案例-利用GLKit加载一张图片

      在OpenGL 中图片其实就是纹理  因此加载图片其实就是加载一张纹理,下面我们就用上文讲到的GLKit来加载纹理并绘制到屏幕上。

     因为OpenGL ES 命令需要渲染上下文和绘图表面才能完成图形图像的绘制,因此利用GLKit绘制图形第一步就是配置好EAGLContext 和 GLKView。具体代码如下:

- (EAGLContext *)bestCreateEAGLContext
{
    EAGLContext *temContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if(!temContext) {
        temContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    }    
    return temContext;
}
- (void)setupConfig 
{
    context = [self bestCreateEAGLContext];
    [EAGLContext setCurrentContext:context];
    GLKView *temView = [[GLKView alloc] initWithFrame:self.view.bounds context:context];
    temView.delegate = self;
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    [self.view addSubview:temView];
}

     在这里准备好了渲染上下文和绘制表面,并设置了清屏颜色。在复杂案例中如果需要设置缓存区的格式,也可以在这里利用GLKView直接配置,比如加上:

temView.drawableDepthFormat = GLKViewDrawableDepthFormat16;
temView.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;

     当然在本例中这些配置都是没有必要的,因为我们仅仅只是想加载一张图片,不涉及深度相关的配置,而颜色缓存区格式默认就是GLKViewDrawableColorFormatRGBA8888 所以也可以不写。

     设置好了渲染上下文、渲染表面等基本条件后,第二步就可以设置渲染需要的参数了,比如顶点坐标,纹理坐标等等,具体代码如下:

- (void)setupVertexData
{    
    GLfloat vertexData[] = {
        0.5f,-0.5f,0.0f,  1.0f, 0.0f,
        0.5f,0.5f,0.0f,   1.0f, 1.0f,
        -0.5f,0.5f,0.0f,  0.0f, 1.0f, 
        -0.5f,0.5f,0.0f,  0.0f, 1.0f,
        -0.5f,-0.5f,0.0f, 0.0f,0.0f,
         0.5f,-0.5f,0.0f, 1.0f, 0.0f,
    };
    GLuint bufferId;
    glGenBuffers(1, &bufferId);
    glBindBuffer(GL_ARRAY_BUFFER, bufferId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GL_FLOAT) * 5, (GLfloat *)NULL + 3);
}

      这里顶点坐标和纹理坐标的设置请参考前一个专题OpenGL相关的文章,需要注意的是vertexData数组是存储在内存中的,所以首先要将内存数据拷贝到显存,而在显存中存储这些数据需要先生成相应的缓冲区;因此我们调用了glGenBuffers和glBindBuffer。那么,这两个函数有什么作用呢?

     void glGenBuffers(GLsizei n,GLuint * buffers);官方的解释为generate buffer object names;意思是生成缓冲区对象的名字,没错仅仅只是个名字,这个函数接收2个参数,第一个表示生成缓冲区名字的个数;第二个参数是用来存储缓冲对象名称的数组。

      个人理解为(如果不对请大神指点)glGenBuffers()函数仅仅是生成一个缓冲对象的名称,这个缓冲对象并不具备任何意义,它仅仅是个缓冲对象,还不是一个顶点数组缓冲,它类似于C语言中的一个指针变量,我们可以分配内存对象并且用它的名称来引用这个内存对象。OpenGL有很多缓冲对象类型,那么这个缓冲对象到底是什么类型,就要用到下面的glBindBuffer()函数了。

     void glBindBuffer(GLenum target,GLuint buffer);官方解释:bind a named buffer object。其第一个参数就是缓冲对象的类型,第二个参数为要绑定的缓冲对象的名称,也就是我们在上一个函数里生成的名称,使用该函数将缓冲对象绑定到OpenGL上下文环境中以便使用。如果把target绑定到一个已经创建好的缓冲对象,那么这个缓冲对象将为当前target的激活对象;但是如果绑定的buffer值为0,那么OpenGL将不再对当前target使用任何缓存对象。 在OpenGL红宝书中有这样一个比喻:绑定对象的过程就像设置铁路的道岔开关,每一个缓冲类型中的各个对象就像不同的轨道一样,我们将开关设置为其中一个状态,那么之后的列车都会驶入这条轨道。 切记:官方文档指出,GL_INVALID_VALUE is generated if buffer is not a name previously returned from a call to glGenBuffers。换句话说,这个名称虽然是GLuint类型的,但是你万万不能直接指定个常量比如说0, 如果你这样做,就会出现GL_INVALID_VALUE的错误。

      绑定缓冲类型后,所有该缓冲类型的函数调用都是用来配置该目标缓冲类型,比如我们这里的顶点缓冲类型GL_ARRAY_BUFFER,glBufferData是通过指定目标缓冲类型来进行数据传输的,而每一个目标缓冲类型再使用前要提前绑定一个缓冲对象,从而赋予这个缓冲对象一个类型的意义,需要注意的是OpenGL允许我们同时绑定多个缓冲类型,只要这些缓冲类型是不同的,换句话说,同一时间,不能绑定两个相同类型的缓冲对象也可以理解为对于一个类型来说,同一时间只能激活一个类型,否则就会发生错误。

      比如绑定了两个相同类型的目标缓冲,数据的配置肯定就会出错。你可以想象一下,假如我们要把数据存入顶点缓冲区,但是顶点缓冲区可以有很多缓冲对象,我需要传入哪个呢,于是我就要提前绑定一个,之后,我只要向顶点缓冲区内传入数据,这个数据就会自动进入被绑定的那个对象里面了,在本例中就是将vertexData数据传入名字为bufferId的缓冲对象中,完成将内存数据拷贝到显存的操作。

       前面已经说过glBufferData的作用将数据从内存拷贝到显存,其函数原型为:void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);第一个参数表示缓冲区类型;第二个参数表示数据字节大小;第三个参数表示需要拷贝的数据;第4个参数表示静态拷贝还是动态拷贝,这里的静态动态之分后面其他的文章会讲到。

       另外,在iOS中默认情况下出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.这意味着顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中). 所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据. 注意: 数据在GPU端是否可见,即着色器能否读取到数据是由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。打开属性通道以后,数据在GPU端可见,但是对于这些数据怎么读取呢,怎么区分哪些是顶点坐标?哪些是纹理坐标呢?这就要说到glVertexAttribPointer函数了。

      先来看一下函数原型:glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)。该函数的功能为明确数据的读取方式。参数index指定要修改的顶点属性的索引值;参数size表示每次读取数量;参数type,指定数组中每个组件的数据类型;参数normalized表示是否需要归一化操作;参数stride,指定连续顶点属性之间的偏移量,如果为0,那么顶点属性会被理解为它们是紧密排列在一起的,比如本例中两个顶点直接的偏移量是5个浮点数所以为sizeof(float) *5;参数ptr指定一个指针,指向数组中第一个顶点属性的第一个组件,比如本例中顶点坐标的第一个顶点属性的第一个组件就是第一个浮点数是所以是(GLfloat *)NULL + 0,又比如本例中纹理坐标的第一个顶点属性的第一个组件就是第4个浮点数是所以是(GLfloat *)NULL + 3。总之利用该函数可以指定出每次读几个数,2个数之间偏移多少 和第一个数的起始位置,这样自然就能确定清楚数据的读取方式。

      好了,上面是参数的配置是本篇的重点所以啰嗦了一点,配置完这些参数后我们就可以进入主题开始第三步加载纹理了。直接上代码:

-(void)setUpTexture

{    //1.获取纹理图片路径
    NSString *filePath = [[NSBundle mainBundle]pathForResource:@"bui" ofType:@"jpg"];
    //2.设置纹理参数    //纹理坐标原点是左下角,但是图片显示原点应该是左上角. 
     NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];
    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];
    //3.使用苹果GLKit 提供GLKBaseEffect 完成着色器工作(顶点/片元)
    cEffect = [[GLKBaseEffect alloc]init];
    cEffect.texture2d0.enabled = GL_TRUE;
    cEffect.texture2d0.name = textureInfo.name;
}

     这一部分的代码比较简单 大家直接看上文代码 和注释就行了,到了这里我们基本的准备工作都做完了可以开始我们最后的工作绘制了。实现GLKViewDelegate的代理方法-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect,来完成最后的绘制工作,代码如下:

-(void)glkView:(GLKView *)view drawInRect:(CGRect)rect 
{
    glClear(GL_COLOR_BUFFER_BIT);
    [mEffect prepareToDraw];
    glDrawArrays(GL_TRIANGLES, 0, 6);
}