简单认识IOS中的OpenGL

1,186 阅读14分钟

一般简单的OpenGL程序一般主要有以下几部分组成:

1、着色器(shader)

着色器一种依赖于GPU硬件的程序,使用着色器语言编写,着色器语言有多种,这里使用的是基于OpenGL的GLSL,关于GLSL的详细信息大家可以自行在网上查阅,提供一个参考GLSL参考

着色器主要处理图像内容显示屏幕上的坐标和颜色。它一般包括顶点着色器和片元着色器,顶点着色器处理显示区域的坐标,而片元着色器处理你将渲染在屏幕的上颜色。

这个需要补充个OpenGL渲染管线的知识,从csdn博客上搬运来的。

OpenGL 渲染管线示意图

上图即为OpenGL渲染管线的基本示意图,可以看出它像一个生产流水线一样,其中灰色部分为本文介绍的可以编程的着色器。

至于其他部分做个简单的介绍:

Primitive Assembly:图元装配,经过着色器处理之后的顶点在该阶段被装配为OpenGL ES支持的基本图元(点、线、三角形),接着对装配好的图元进行裁剪,保留完全在视锥体中的图元,丢弃完全不在视锥体中的图元,对一半在一半不在的图元进行裁剪;接着再对在视锥体中的图元进行剔除处理;这个过程可编码来决定是剔除正面,背面还是全部剔除。

Rasterization:光栅化。在光栅化阶段,基本图元被转换为二维的片元(fragment),fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。 Per-Fragment Operation:在这一阶段对片元着色器输出的每一个片元进行一系列测试与处理,从而决定最终用于渲染的像素。

顶点着色器(Vertex Shader)
 attribute vec4 position;
 attribute vec2 texCoord;
 varying vec2 v_textureCoordinate;
 uniform mat4 modelViewProjectionMatrix;
 void main() {
     v_textureCoordinate = texCoord;
     gl_Position = modelViewProjectionMatrix * position;
 }

attribute是属性限定符,其修饰的变量用来接收渲染管线传递进顶点着色器的当前待处理顶点的各种属性值。用attribute限定符修士的变量其值是由宿主程序批量出入渲染管线的,管线进行基本处理后再传递给顶点着色器。数据中有多少个顶点,管线就调用多少次顶点着色器,每次将一个顶点的各种属性数据传递给顶点着色器中对应atribute变量。因此,顶点着色器每次执行将完成对一个顶点各项属性数据的处理。

uniform为一致变量限定符,一致变量指的是对于同一组顶点组成的单个3D物体中所有顶点都相同的量。Uniform变量可以用在顶点着色器或片元着色器中,其支持用来修饰所有的基本数据类型。一致变量的值也是从宿主程序传入的。

varying表示用于从顶点着色器传递到片元着色器的量。

vec4表示包含4个浮点数的向量,同理vec2表示包含2个浮点数的向量。由于GPU的特性,在运算中会大量使用向量和矩阵。

mat4表示一个4*4的浮点数矩阵。

gl_Position为系统内建变量,表示原始的顶点数据在顶点着色器中经过平移、旋转、缩放等数学变换后,生成新的顶点位置。这些内建变量不需要声明就可以使用,一般用来实现渲染管线固定功能部分与自定义顶点或片元着色器之间的信息交互。顶点着色器中的内建变量主要是输出变量,包括gl_Position、gl_PointSize等。在顶点着色器中应该根据需要给这些内建变量赋值,以便由渲染管线中的图元装配与光栅化等后续固定功能阶段进行进一步的操作。

片元着色器(fragment shader)
 precision mediump float;
 
 uniform sampler2D SamplerY;
 uniform sampler2D SamplerUV;
 
 varying mediump vec2 v_textureCoordinate;
 
 uniform mat3 colorConversionMatrix;
 
 void main() {
     mediump vec3 yuv;
     lowp vec3 rgb;
     
     yuv.x = texture2D(SamplerY, v_textureCoordinate).r - (16.0/255.0);
     yuv.yz = texture2D(SamplerUV, v_textureCoordinate).rg - vec2(0.5, 0.5);
     
     rgb = colorConversionMatrix * yuv;
     
     gl_FragColor = vec4(rgb, 1);
 }

precision precision-qualifier type表示指定默认精度。precision可以用来确定默认精度修饰符。type可以是int或float或采样器类型,precision-qualifier可以是lowp, mediump, 或者highp。顶点着色器是有默认精度的,而片元着色器没有。各个精度的标准如下:

GLSL 精度

sampler2D:sample表示采样器类型,实际上是纹理的不透明句柄。2D表示纹理的类型是2D的。它们用在内建的纹理函数来指明要访问哪一个纹理。它们只能被声明为函数参数或uniforms。除了纹理查找函数参数, 数组索引, 结构体字段选择和圆括号外,取样器不允许出现在表达式中。

texture2D:纹理查找函数。用于实现从指定纹理中查找某个坐标的纹理数据。

colorConversionMatrix:颜色变换矩阵,由OpenGL ES API指定。在解码视频或者流数据,我们一般会得到YUV数据,YUV到RGB颜色空间的转换可以通过颜色变换矩阵实现。当然,你也可以自己手动计算,具体方式可自行查阅。

如果只是显示一个RGB的图片,可以使用下面的shader:
precision mediump float;
 
 uniform sampler2D Texture;
 
 varying vec2 v_textureCoordinate;
 
 void main() {
     gl_FragColor = texture2D(Texture, v_textureCoordinate);
 }

2、连接程序(program)

用于连接你的shader和OpenGLes程序,并处理两者之间的数据交互。目前在项目中主要作用是根据不同的需求加载不同的shader,处理变换矩阵。比如你显示不同的数据类型时RGB和YUV等等,你就需要有不同的shader来处理。当然,你也可以选择使用一个shader,然后在shader里编程,然而我不会,就没那么做。

本文通过分析GPUImage库的GLProgram类,整理下相关知识。

创建program
      ```GLuint program = glCreateProgram();```

该函数将创建一个空的program对象,并返回一个非零的引用标识。program对象可以附加和移除着色器对象,一般包含顶点着色器和片元着色器。program会根据附加的着色器对象创建一些可执行文件,并使之成为当前渲染环境的一部分。

附加着色器
- (BOOL)compileShader:(GLuint *)shader 
                 type:(GLenum)type 
               string:(NSString *)shaderString
{
//    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();

    GLint status;
    const GLchar *source;
    
    source = 
      (GLchar *)[shaderString UTF8String];
    if (!source)
    {
        NSLog(@"Failed to load vertex shader");
        return NO;
    }
    
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);
    
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);

	if (status != GL_TRUE)
	{
		GLint logLength;
		glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
		if (logLength > 0)
		{
			GLchar *log = (GLchar *)malloc(logLength);
			glGetShaderInfoLog(*shader, logLength, &logLength, log);
            if (shader == &vertShader)
            {
                self.vertexShaderLog = [NSString stringWithFormat:@"%s", log];
            }
            else
            {
                self.fragmentShaderLog = [NSString stringWithFormat:@"%s", log];
            }

			free(log);
		}
	}	
	
//    CFAbsoluteTime linkTime = (CFAbsoluteTimeGetCurrent() - startTime);
//    NSLog(@"Compiled in %f ms", linkTime * 1000.0);

    return status == GL_TRUE;
}

glCreateShader函数创建一个空的着色器对象,并返回一个非零的引用标识。着色器对象用于维护定义着色器的源代码字符串。可以传入GL_VERTEX_SHADER或GL_FRAGMENT_SHADER来创建顶点着色器和片元着色器。着色器对象和与之关联的数据,可以在共享上下文的环境中共享。

glShaderSource(shader,arrCount,stringArr,lengthArr)将着色器对象的源码指定为目标字符串数组中的源码,着色器对象的原有内容将被完全替换。如果长度数组为null,则假定每个字符串都是空终止的。如果不是null,源代码字符串将不被扫描或解析,只是复制到指定的着色器对象中。OpenGL复制着色器源代码字符串,因此你可以在函数返回后立即释放它。

但是,请注意Shader编译器支持是可选的,如果不确定是否支持,可以使用参数GL_SHADER_COMPILER调用glGet来查询。glShaderSource,glCompileShader,glGetShaderPrecisionFormat和glReleaseShaderCompiler将在shader编译器不支持时,生成GL_INVALID_OPERATION。

glCompileShader会编译已存储在由着色器指定的着色器对象中的源代码字符串,并将编译结果保存。你可以通过glGetShaderiv来查询。无论编译是否成功,有关编译的信息都可以通过调用glGetShaderInfoLog从着色器对象的信息日志中获取。

glGetShaderiv可以查询shader对象的状态信息。参数包括GL_SHADER_TYPE,GL_DELETE_STATUS,GL_COMPILE_STATUS,GL_INFO_LOG_LENGTH(存储信息日志所需的字符缓冲区大小),GL_SHADER_SOURCE_LENGTH(存储着色器源码所需的字符缓冲区的大小)。

    glAttachShader(program, vertShader);

glAttachShader将着色器对象附加到指定的program对象上,以便在连接时生成可执行文件。附加操作在着色器对象生成后,就可以进行。这意味着你可以着色器对象加载源码之前,就将它附加到program对象。 多个相同类型的着色器对象可能不会附加到单个程序对象。但是,单个着色器对象可能会附加到多个程序对象。如果着色器对象在附加到程序对象时被删除,它将被标记为删除,并且直到调用glDetachShader才能将其从它所连接的所有程序对象中分离出来,删除将不会发生。

连接program
    glLinkProgram(program);
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    glUseProgram(program);

glLinkProgram执行链接操作,并保存链接状态。shader将根据源码创建可执行文件,所有与program相关的用户定义的uniform将被初始化为0,并且生成一个可以访问的地址,你可以通过调用glGetUniformLocation来查询。所有未绑定到顶点属性索引的attribute,此时都将被链接器绑定。分配的位置可以通过调用glGetAttribLocation来查询。链接操作之后,program可以自由修改附加的着色器对象,编译附加的着色器对象,分离着色器对象,删除着色器对象以及附加其他着色器对象。

在调用glUseProgram之后(program已在使用中),执行链接操作,如果链接成功,glLinkProgram还会将生成的可执行文件安装为当前渲染状态的一部分;如果链接失败,其链接状态将设置为GL_FALSE,但可执行文件和关联状态将保持为当前状态的一部分,直到随后对glUseProgram的调用将其删除而不再使用为止。从使用中删除后,在成功重新链接之前,它不能成为当前状态的一部分。

由于OpenGL ES着色语言规范中规定的多种原因,以下情况链接可能会失败:顶点着色器和片段着色器都不在程序对象中、已超过实施支持的活动属性变量的数量、统一变量的存储限制已被超出、实施支持的有效统一变量的数量已被超过、顶点着色器或片段着色器缺少主要功能、在片段着色器中实际使用的变量变量在顶点着色器中没有以相同方式声明(或者根本没有声明)、对函数或变量名称的引用未解决、共享的全局声明有两种不同的类型或两种不同的初始值、一个或多个附加的着色器对象尚未成功编译(通过glCompileShader),或者加载了预编译的着色器二进制文件(通过glShaderBinary)、绑定通用属性矩阵会导致矩阵的某些行落在允许的最大值GL_MAX_VERTEX_ATTRIBS之外、找不到足够的连续顶点属性槽来绑定属性矩阵。

glUseProgram设置program为当前渲染状态的一部分。如果program为0,当前渲染状态指向无效的program,任何glDrawArrays或glDrawElements命令都会提示未定义。program对象和与之关联的数据,可以在共享上下文的环境中共享。

通过program获取着色器的属性的位置
- (void)addAttribute:(NSString *)attributeName
{
    if (![attributes containsObject:attributeName])
    {
        [attributes addObject:attributeName];
        glBindAttribLocation(program, 
                             (GLuint)[attributes indexOfObject:attributeName],
                             [attributeName UTF8String]);
    }
}
// END:addattribute
// START:indexmethods
- (GLuint)attributeIndex:(NSString *)attributeName
{
    return (GLuint)[attributes indexOfObject:attributeName];
}
- (GLuint)uniformIndex:(NSString *)uniformName
{
    return glGetUniformLocation(program, [uniformName UTF8String]);
}

glBindAttribLocation(program,index,name)用于将由program对象中的用户定义的属性变量与通用顶点属性索引相关联。name为以空结尾的字符串。index指定要绑定到此变量的通用顶点属性索引。该命令使顶点着色器可以为属性变量使用描述性名称,而不是从0到GL_MAX_VERTEX_ATTRIBS-1编号的通用变量。当程序成为当前状态的一部分时,通过通用顶点属性索引提供的值将修改由name指定的用户定义属性变量的值。发送到每个通用属性索引的值是当前状态的一部分,就像标准顶点属性(如颜色,法线和顶点位置)一样。如果通过调用glUseProgram使一个不同的程序对象成为当前对象,那么新program对象中也会绑定到索引的属性到相同的值。

如果name引用矩阵属性变量,则index引用矩阵的第一列。其他矩阵列然后自动绑定到index+n。通过调用glBindAttribLocation,可以随时显式分配程序对象的属性变量名称到通用属性索引绑定。在调用glLinkProgram之前,属性绑定不会生效。程序对象成功链接后,通用属性的索引值保持不变,直到下一次执行链接命令。应用程序不允许使用此命令绑定任何标准OpenGL顶点属性,因为它们在需要时会自动绑定。在程序对象链接后发生的任何属性绑定将在下次链接程序对象时才生效。

如果name之前被绑定,再次绑定时,以前的绑定信息将丢失。因此,不能将一个用户定义的属性变量绑定到多个索引,但可以将多个用户定义的属性变量绑定到同一个索引。

glGetUniformLocation返回program对象内指定名称的uniform的位置,名称必须是不包含空格的空终止字符串。如果uniform不存在,或者名称以"gl"开头,则返回-1;在program成功链接之前,分配给统一变量的实际位置是未知的。发生链接后,可以使用命令glGetUniformLocation来获取统一变量的位置。然后可以将此位置值传递给glUniform以设置统一变量的值或glGetUniform以查询统一变量的当前值。程序对象成功链接后,统一变量的索引值保持固定,直到发生下一个链接命令。如果链接成功,则只能在链接后查询统一的变量位置和值。

3、渲染环境

配置OpenGL es的渲染环境,主要包括设置OpenGL版本,创建OpenGL的各种缓存空间,通过program加载shader等等操作。我把这些操作放在一起,组成了一个manager。

4、GLKViewController

apple提供给我们的用于配合OpenGLes渲染流程的视图控制器。它集合了视图、渲染流程控制的功能。

使用GLKViewController需要自己子类化。

init方法
//根据显示需求,创建不同类型的子类
-(instancetype)initWithType:(xxxx)type {
    self = [super init];
    if (self) {
               //我在这里根据type类型初始化glManager
              //glManager是我自己根据不同的OpenGL es环境封装的
        }
    }
    return self;
}
指定glkview的context和帧率等信息
-(void)viewDidLoad{
    [super viewDidLoad];

    GLKView *view = (GLKView *)self.view;     //获取GLKVIEW
    view.backgroundColor = [UIColor clearColor];
    view.context = [self.glManager getGLESContext]; 
    [self.glManager initGLES];
    //    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888; //颜色模式
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;   //深度模式
    view.contentScaleFactor = [UIScreen mainScreen].scale;     //缩放比例
    self.preferredFramesPerSecond = 30;    //帧率
    [self configGesture];
}
实现glkViewdelete协议的方法,来绘制图像
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    //根据需求实现自己的绘制方法
    //绘制图像
    UIImage *image = [self.v360DrawingDelegate retrieveImageToDraw];
    [self.glManager draw360Image:image InView:self.view];
    //YUV数据
    //H264YUV_Frame *frame = [self.v360DrawingDelegate retrieveYUVFrameToDraw];
    //[self.glManager drawYUVFrame:frame InView:self.view];
    //硬解码后的CVPixelBufferRef
    //CVPixelBufferRef pixelBuffer = [self.v360DrawingDelegate retrievePixelBufferToDraw];
   //[self.glManager draw360Buffer:pixelBuffer InView:self.view];
}

GLKViewController在刷新每一帧之前,会调用它自己协议的glkViewControllerUpdate做一些更新操作,比如更新变换矩阵等。在其子类中,你可以通过-(void)update来覆盖它。

#pragma mark - GLKViewController Subclass
- (void)update {
    if (self.glManager == NULL) {
        return;
    }
    float aspect = self.view.bounds.size.width / self.view.bounds.size.height;
    //更新变换矩阵
}
定制你的需求

我在controller里实现了滑屏处理和缩放手势处理。

    //手势处理
    UIPinchGestureRecognizer *pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
    pinchRecognizer.delegate = self;
    [self.view addGestureRecognizer:pinchRecognizer];
    
    UITapGestureRecognizer *singleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTapGesture:)];
    singleTapRecognizer.numberOfTapsRequired = 1;
    singleTapRecognizer.delegate = self;
    singleTapRecognizer.numberOfTouchesRequired = 1;
    [self.view addGestureRecognizer:singleTapRecognizer];


  //滑屏处理
  - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *touch in touches) {
        [_currentTouches addObject:touch];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    float startX = [touch previousLocationInView:touch.view].x;
    float startY = [touch previousLocationInView:touch.view].y;
    float distX = [touch locationInView:touch.view].x - startX;
    float distY = [touch locationInView:touch.view].y - startY;
    //自己的处理逻辑,处理变换矩阵
        return;
    }
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *touch in touches) {
        [self.currentTouches removeObject:touch];
    }
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    for (UITouch *touch in touches) {
        [self.currentTouches removeObject:touch];
    }
}

5、解码器

如果你需要播放h264流等视频数据的话,你还需要一个解码器。如果采用硬解码的话,可以用官方API;软解码的话可以用ffmpeg。