iOS视觉(九) -- OpenGL ES初探

756 阅读11分钟

一、OpenGL ES简介

**OpenGL ES(OpenGL for Embedded Systems)**是以手持和嵌入式为目标的高级3D图形应用程序编程接口。 OpenGL ES是目前智能手机中占据统治地位的图形API。

支持的平台有:

  • iOS
  • Android
  • BlackBerry
  • bada
  • linux
  • Windows

OpenGL ES苹果文档:

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

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

OpenGL ES允许应⽤程序利⽤底层图形处理器的强⼤功能。iOS设备上的GPU可以执⾏复杂的2D和3D绘图,以及最终图像中每个像素的复杂着⾊计算。

二、OpenGL ES流程

OpenGL ES与OpenGL的流程是一致的:

我们通过API去操作顶点数据、着色器等,再进行图元装配、光栅化等,最后在屏幕上渲染出来。

2.1、OpenGL ES图形管道

在Application里,也就是我们所写的代码里来进行提供图元装配顶点信息和图片信息。

在顶点着色器中来确定图形的顶点、缩放信息。我们所做的图形的平移缩放等都是基于图形的顶点来实现的。

接下来来到图元装配,如果图像超出显示区域,我们则需要进行裁剪。

处理后来到片元着色器的阶段,我们就需要对每一个像素点来进行处理,例如纹理的添加。

最后来到帧缓冲区,来进行一些混合、深度测试等处理。

2.2、顶点着色器

顶点着⾊器:

  1. 着⾊器程序—描述顶点上执⾏操作的顶点着⾊器程序源代码/可执⾏⽂件
  2. 顶点着⾊器输⼊(属性)—⽤顶点数组提供每个顶点的数据
  3. 统⼀变量(uniform)—顶点/⽚元着⾊器使⽤的不变数据
  4. 采样器—代表顶点着⾊器使⽤纹理的特殊统⼀变量类型.

我们可以对着色器进行输入属性,并得到输出属性:

顶点着⾊器业务:

  1. 矩阵变换位置
  2. 计算光照公式⽣成逐顶点颜⾊
  3. ⽣成/变换纹理坐标

总结:它可以⽤于执⾏⾃定义计算,实施新的变换,照明或者传统的固定功能所不允许的基于顶点的效果.

2.3、图元装配

顶点着⾊器之后,下⼀个阶段就是图元装配.

图元(Primitive): 点,线,三⻆形等.

图元装配: 将顶点数据计算成⼀个个图元.在这个阶段会执⾏裁剪、透视分割和 Viewport变换操作。

图元类型和顶点索确定将被渲染的单独图元。对于每个单独图元及其对应的顶点,图元装配阶段执⾏的操作包括:将顶点着⾊器的输出值执⾏裁剪、透视分割、视⼝变换后进⼊光栅化阶段。

2.4、光栅化

在这个阶段绘制对应的图元(点/线/三⻆形).

光栅化就是将图元转化成⼀组⼆维⽚段的过程.⽽这些转化的⽚段将由⽚元着⾊器处理.这些⼆维⽚段就是屏幕上可绘制的像素.

2.5、片元着色器

⽚元着⾊器/⽚段着⾊器:

  1. 着⾊器程序—描述⽚段上执⾏操作的⽚元着⾊器程序源代码/可执⾏⽂件
  2. 输⼊变量—光栅化单元⽤插值为每个⽚段⽣成的顶点着⾊器输出
  3. 统⼀变量(uniform)—顶点/⽚元着⾊器使⽤的不变数据
  4. 采样器—代表⽚元着⾊器使⽤纹理的特殊统⼀变量类型.

在片元着色器中的输入变量, 则是由顶点着色器桥接后输入的变量:

⽚元着⾊器业务:

  1. 计算颜⾊
  2. 获取纹理值
  3. 往像素点中填充颜⾊值(纹理值/颜⾊值);

总结: 它可以⽤于图⽚/视频/图形中每个像素的颜⾊填充(⽐如给视频添加滤镜,实际上就是将视频中每个图⽚的像素点颜⾊填充进⾏修改.)

2.6、逐片段操作

  • 像素归属测试: 确定帧缓存区中位置(Xw,Yw)的像素⽬前是不是归属于OpenGLES所有.例如,如果⼀个显示OpenGLES帧缓存区View被另外⼀个View所遮蔽.则窗⼝系统可以确定被遮蔽的像素不属于OpenGLES上下⽂.从⽽不全显示这些像素.⽽像素归属测试是OpenGLES的⼀部分,它不由开发者开⼈为控制,⽽是由OpenGLES内部进⾏.
  • 裁剪测试: 裁剪测试确定(Xw,Yw)是否位于作为OpenGLES状态的⼀部分裁剪矩形范围内.如果该⽚段位于裁剪区域之外,则被抛弃.
  • 深度测试: 输⼊⽚段的深度值进步⽐较,确定⽚段是否拒绝测试
  • 混合: 混合将新⽣成的⽚段颜⾊与保存在帧缓存的位置的颜⾊值组合起来.
  • 抖动: 抖动可⽤于最⼩化因为使⽤有限精度在帧缓存区中保存颜⾊值⽽产⽣的伪像.

三、EGL(Embedded Graphics Library)系统

OpenGLES命令需要渲染上下⽂和绘制表⾯才能完成图形图像的绘制.

:绘制表⾯是⽤于绘制图元的表⾯,它指定渲染所需要的缓存区类型,例如颜⾊缓存区,深度缓冲区和模板缓存区.)

OpenGL ES API并没有提供如何创建渲染上下⽂或者上下⽂如何连接到原⽣窗⼝系统.

EGL是Khronos渲染API(如OpenGLES)和原⽣窗⼝系统之间的接⼝.唯⼀⽀持OpenGLES却不⽀持EGL的平台是iOS. Apple提供⾃⼰的EGLAPI的iOS实现,称为EAGL.

因为每个窗⼝系统都有不同的定义,所以EGL提供基本的不透明类型—EGLDisplay,这个类型封装了所有系统相关性,⽤于和原⽣窗⼝系统接⼝.

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

四、GLKit概述

GLKit 框架的设计⽬标是为了简化基于OpenGL / OpenGL ES 的应⽤开发.它的出现加快OpenGL ES或OpenGL应⽤程序开发。

使⽤数学库,背景纹理加载,预先创建的着⾊器效果,以及标准视图和视图控制器来实现渲染循环。GLKit框架提供了功能和类,可以减少创建新的基于着⾊器的应⽤程序所需的⼯作量,或者⽀持依赖早期版本的OpenGL ES或OpenGL提供的固定函数顶点或⽚段处理的现有应⽤程序。

  • GLKView 提供绘制场所(View)

  • GLKViewController(扩展于标准的UIKit设计模式. ⽤于绘制视图内容的管理与呈现.)

苹果弃⽤OpenGL ES ,但iOS开发者可以继续使⽤.

在GLKit中,会为我们节省许多的代码,让开发者所做的事情更加的简洁。

GLKit的主要功能:

  • 加载纹理
  • 提供⾼性能的数学运算
  • 提供常⻅的着⾊器
  • 提供视图以及视图控制器

GLKit中为我们封装了许多的API,所以我们可以直接使用这些API。

五、使用GLKit实现图片加载

在iOS中,我们使用UIImageView就可以直接加载一张图片并显示在屏幕上。 这里通过使用GLKit来实现一下

5.1、GLKit绘制空白界面

在XCode中,新建一个iOS工程后,在ViewController中导入GLKit库,并修改当前控制器继承于GLKViewController:

#import <GLKit/GLKit.h>
@interface ViewController :GLKViewController
@end

并且还需要在Main.storyboard中修改继承UIView为GLKView。

在ViewController中,导入OpenGL ES库后,创建一个上下文:

#import "ViewController.h"
#import <OpenGLES/ES3/gl.h>
#import <OpenGLES/ES3/glext.h>

@interface ViewController ()
{
    EAGLContext *context;
}
@end

接下来在viewDidLoad中就需要进行以下相关代码配置:

- (void)setUpConfig {
    //创建上下文
    context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (!context) {
        NSLog(@"创建上下文错误!");
        return;
    }
    //设置当前上下文(当前上下文只有一个)
    [EAGLContext setCurrentContext:context];
    
    //设置GLKView
    GLKView *view = (GLKView *)self.view;
    view.context = context;
    
    //设置背景颜色
    glClearColor(0.7, 0.7, 0.7, 1);
}

在进行实现绘制代理来清除缓冲区:

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    glClear(GL_COLOR_BUFFER_BIT);
}

这样我们就创建好了一个用GLKit绘制背景的空工程。

5.2、GLKit加载纹理

首先需要声明一个着色系统,GLKBaseEffect, 用于基于着色器OpenGL渲染。

声明一个成员变量:

GLKBaseEffect *cEffect;

接下来就要对GLView进行颜色、深度缓冲区的设置:

    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;//颜色缓冲区
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;//深度缓冲区

接下来需要创建顶点数据:

- (void)setUpVertexData {

    /*
     纹理坐标系取值范围[0,1];原点是左下角(0,0);
     故而(0,0)是纹理图像的左下角, 点(1,1)是右上角.
     */
    //前三个数据为 xyz。后面2个为纹理坐标st
    GLfloat vertexData[] = {
        
        0.5, -0.5, 0.0f,    1.0f, 0.0f, //右下
        0.5, 0.5,  0.0f,    1.0f, 1.0f, //右上
        -0.5, 0.5, 0.0f,    0.0f, 1.0f, //左上
        
        0.5, -0.5, 0.0f,    1.0f, 0.0f, //右下
        -0.5, 0.5, 0.0f,    0.0f, 1.0f, //左上
        -0.5, -0.5, 0.0f,   0.0f, 0.0f, //左下
    };
    
    //创建顶点缓冲区(在GPU中)
    GLuint bufferID;
    glGenBuffers(1, &bufferID);
    
    //绑定纹理缓冲区
    glBindBuffer(GL_ARRAY_BUFFER, bufferID);
    
    //将顶点数据从内存中拷贝到GPU
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
    
    //打开顶点坐标数据通道
    glEnableVertexAttribArray(GLKVertexAttribPosition);

    //设置顶点数据的读取方式
    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, 0);
    
    //打开纹理坐标数据通道
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 3);
    
}

在iOS中, 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.意味着,顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中).所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据.

注意: 数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。

设置数据读取方式:

glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * ptr)

功能: 上传顶点数据到显存的方法(设置合适的方式从buffer里面读取数据)

参数列表:

  • index,指定要修改的顶点属性的索引值,例如
  • size, 每次读取数量。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),纹理则是2个.)
  • type,指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, * * * * * GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。
  • normalized,指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)
  • stride,指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0
  • ptr指定一个指针,指向数组中第一个顶点属性的第一个组件。初始值为0

接下来就是设置纹理相关绘制的:

- (void)setUpTexture {
    //设置路径
    NSString *filePaht = [[NSBundle mainBundle] pathForResource:@"2" ofType:@"jpg"];
    
    //设置纹理参数
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1), GLKTextureLoaderOriginBottomLeft, nil];
    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePaht options:options error:nil];
    
    //加载纹理
    cEffect = [[GLKBaseEffect alloc] init];
    cEffect.texture2d0.enabled = GL_TRUE;//开始使用纹理
    cEffect.texture2d0.name = textureInfo.name;//设置纹理名字
}

最后运行程序实现效果:

注: 此处由于顶点数据设置均为0.5,即均为屏幕一半,将之全部修改为1即可全屏显示