音视频开发:OpenGL + OpenGL ES + Metal 系列文章汇总
OpenGL ES(OpenGL for Embedded Systems)是当前使用最广泛的图形API,是OpenGL的简化版本,消除了冗余功能,提供了一个既易于学习,又易于在移动图形硬件中实现的库,以手持和嵌入式为目标的高级3D图形应用编程接口。本文开启OpenGL ES的学习,熟悉OpenGL ES的渲染流程,并学习GLKit的简单使用。
主要内容:
1、渲染流程(重点)
2、GLKit的学习(熟悉)
3、GLKit实现图片加载的案例
1、渲染流程
需要了解将数据转换为帧并显示到屏幕上都要经历哪些流程,每个流程的具体内容,以及我们可操作的部分有哪些,如何进行操作。
重点掌握整体流程和顶点着色器和片元着色器的使用
1.1 客户端-服务器端体系结构
在大的范围来看,渲染流程分成两部分客户端和服务器端,客户端运行在CPU中,服务器端运行在GPU中。
客户端包括OpenGL ES Framework,作为中间桥梁,app代码通过OpenGL ES API,会调度OpenGL ES Framework,OpenGL ES client 调度 OpenGL ES server将顶点数据等传递到GPU。
服务器端做一些图形硬件的处理,包括着色器、图元装配、光栅化等。
1.2 图形管道
这里按照功能模块简述了简单的重要的过程:获取数据->顶点处理->构造几何图形->片元处理->对帧数据的处理。
1、Application:客户端传入的数据包括用作图元装配的信息(顶点数据,图元类型等)和图像数据。
2、Vertex:顶点的处理包括进行图形变换和光照效果(因此变换矩阵的计算是在顶点着色器中实现)。
3、Geometry:构造几何图形包括图元装配和裁剪,图元装配是对顶点按照图元的方式进行构造,也就是绘制的过程;超出范围的要进行裁剪。
4、Fragment:片元的处理包括设置纹理和颜色,还有雾化处理。设置纹理需要纹理数据和纹理坐标。
5、Framebuffer Operations:帧缓存区操作是对帧数据的处理,处理完毕后就可以显示到屏幕上了。包括设置透明度、模板、深度测试,之后进行混合,这些操作都是在即将显示时,在帧缓冲区中完成的动作。
1.3 管道流程的理解
1.3.1 理解
- 传入的数据包括顶点数据、纹理数据、纹理坐标
- 顶点数据在内存中存储在顶点数组中,还可以拷贝到显存中的顶点缓存区中
- 光栅化是将图元转化成二维片段
- 我们可编程部分只有顶点着色器和片元着色器
- 纹理坐标需要通过顶点着色器传递到片元着色器
1.3.2 顶点着色器
它描述针对顶点上执行操作的顶点着色器程序源代码/可执行文件,可以用于执行自定义计算,实施新的变换,照明或者传统的固定功能所不允许的基于顶点的效果。简单说就是着色器是执行在GPU上的一段代码段。
输入输出:
输入:
- 通过attribute通道传输,提供每个顶点的数据
- 通过uniform通道传入统一变量,顶点/片元着色器使用的不变数据
- 采样器,表示顶点着色器使用纹理的特殊统一变量类型,用来传输纹理数据
输出:
gl_Position:最终得到顶点数据
gl_PointSize:得到的顶点大小
作用:
- 矩阵变换位置
- 计算光照公式生成逐顶点颜色(也可以片元着色器)
- 生成/变换纹理坐标(片元着色器是没有办法传入属性即attribute的,可以通过顶点着色器桥接,间接将纹理坐标属性传递到片元着色器)
代码示例:
attribute vec4 position;
attribute vec2 textCoordinate;
uniform mat4 rotateMatrix;
varying lowp vec2 varyTextCoord;
void main()
{
varyTextCoord = textCoordinate;
vec4 vPos = position;
vPos = vPos * rotateMatrix;
gl_Position = vPos;
}
- 详细的后面在将GLSL时会说明,这里只简单说明
- 我们现在可以理解的是传入的参数中包括attribute和uniform
- attribute的数据包括顶点数据和纹理数据
- uniform的数据有旋转矩阵
- 在计算时可以通过顶点数据叉乘旋转矩阵,并通过gl_Position来接收计算后的数据
1.3.3 图元装配
简单介绍: 将顶点数据计算成一个一个图元,图元类型和顶点共同确定将被渲染的单独图元。
每个顶点以及组成的每个单独图元,在装配阶段执行的操作包括:将顶点着色器的输出值执行裁剪、透视分割、视口变换
详情可以查看图元绘制
1.3.4 光栅化
简单介绍: 光栅化就是将图元转化成一组二维片段的过程,这些二维片段在经过片元着色器处理后就是屏幕上显示的像素。
1.3.5 片元着色器
它是描述片段上执行操作的片元着色器程序源代码/可执行文件,也叫片段着色器或像素着色器。
输入输出:
不能值直接输入属性,只能是通过顶点着色器传过来的,经过光栅化阶段进入到片元着色器
输入:
- 不能值直接输入属性,只能是通过顶点着色器传过来的,经过光栅化阶段进入到片元着色器
- 统一变量(uniform)是片元着色器使用的不变数据
- 采样器是片元着色器使用纹理的特殊统一变量类型
输出:
通过gl_FragColor接收
作用: 1)计算颜色、2)获取纹理值、3)往像素点中填充颜色值(纹理直/颜色值)
简单来说就是给图片/视频中每个像素的颜色填充。
代码示例:
varying lowp vec2 varyTextCoord;
uniform sampler2D colorMap;
void main() {
gl_FragColor = texture2D(colorMap, varyTextCoord); }
- 详细的在学习GLSL时会讲解
- 这里可以看到拿到的数据有纹理坐标还有uniform直接获取的色值(varying表示从顶点着色器获取的)
- 最后会用gl_FragColor接收结果
1.3.6 逐片段操作
只要知道此时有这么些操作即可
2、EAGL
EAGL是苹果提供的切入式图形库。
OpenGL ES命令需要渲染上下文和绘制表面才能完成图形图像的绘制,但是OpenGL ES 为了可移植,没有提供如何创建渲染上下文或者上下文如何连接到原生窗口系统。因此出现了EAGL帮我们完成渲染上下文和绘制表面的工作。
它是对EGL的二次封装,EGL(Embedded Graphics Library)是用于和原生窗⼝系统接⼝。
注:
渲染上下文用来存储相关OpenGL ES状态
绘制表面用于绘制图元的表面,它指定渲染所需要的缓存区类型,例如颜色缓存区、深度缓存区、模板缓存区
3、GLKit的认识
GLKit库提供了大量的功能和类供开发者使用,简化程序开发。对于简单的功能可以使用GLkit,这样可以大大减少代码量和代码复杂度。当然对于复杂的功能它就力不从心了。
至于常用的API,以及GLKit的使用流程会用一个案例来分析。
3.1 功能:
- 加载纹理
- 提供高性能的数学运算
- 提供常见的着色器
- 提供视图以及视图控制器
视图GLKView提供绘制场所,继承自UIView;视图控制器GLKViewController用于绘制视图内容的管理和呈现,继承自UIViewController。
3.2 常用API
请查看文档。常用API
4、GLKit实现图片的加载
案例详情请查看带纹理图片的加载
4.1 前提介绍
功能:
- 渲染背景色
-
加载一张图片
需要学习掌握:
- 界面初始化过程
- 颜色渲染
- 坐标加载
- 纹理加载
- 渲染代理方法 - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
4.2 准备工作
创建一个iOS项目,并将系统创建的ViewController的父类由UIViewController修改为GLKViewController,其中的view的父类由UIView修改为GLKView
在ViewController.h文件中导入GLKit框架的头文件#import
在ViewController.h文件中导入Opengl ES相关头文件#import
4.3 分模块分析
4.3.1 viewDidLoad整体调用
可以看出我们做了三件事
- OpenGL ES的初始化工作
- 加载顶点/纹理坐标数据
- 加载纹理数据
代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//1.OpenGL ES 相关初始化
[self setUpConfig];
//2.加载顶点/纹理坐标数据
[self setUpVertexData];
//3.加载纹理数据(使用GLBaseEffect)
[self setUpTexture];
}
4.3.2 setUpConfig初始化工作
- 设置上下文环境
- 初始化GLKView
- GLKView的缓存格式配置(也可以看做是初始化的一部分)
- 颜色设置
注释足够详细,这些步骤基本是固定的,只要记得即可
-(void)setUpConfig
{
//1.初始化上下文&设置当前上下文
/*
EAGLContext 是苹果iOS平台下实现OpenGLES 渲染层.
kEAGLRenderingAPIOpenGLES1 = 1, 固定管线
kEAGLRenderingAPIOpenGLES2 = 2,
kEAGLRenderingAPIOpenGLES3 = 3,
*/
//一般用2或用3没有什么太大差别
context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES3];
//判断context是否创建成功
if (!context) {
NSLog(@"Create ES context Failed");
}
//设置当前上下文
//上下文可以有多个,一次只能用一个,所以需要设置当前的上下文
[EAGLContext setCurrentContext:context];
//2.获取GLKView & 设置context
GLKView *view =(GLKView *) self.view;
view.context = context;
/*3.配置视图创建的渲染缓存区.
(1). drawableColorFormat: 颜色缓存区格式.
简介: OpenGL ES 有一个缓存区,它用以存储将在屏幕中显示的颜色。你可以使用其属性来设置缓冲区中的每个像素的颜色格式。
GLKViewDrawableColorFormatRGBA8888 = 0,
默认.缓存区的每个像素的最小组成部分(RGBA)使用8个bit,(所以每个像素4个字节,4*8个bit)。
GLKViewDrawableColorFormatRGB565,
如果你的APP允许更小范围的颜色,即可设置这个。会让你的APP消耗更小的资源(内存和处理时间)
(2). drawableDepthFormat: 深度缓存区格式
GLKViewDrawableDepthFormatNone = 0,意味着完全没有深度缓冲区
GLKViewDrawableDepthFormat16,
GLKViewDrawableDepthFormat24,
如果你要使用这个属性(一般用于3D游戏),你应该选择GLKViewDrawableDepthFormat16
或GLKViewDrawableDepthFormat24。这里的差别是使用GLKViewDrawableDepthFormat16
将消耗更少的资源
*/
//3.配置视图创建的渲染缓存区.
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
view.drawableDepthFormat = GLKViewDrawableDepthFormat16;
//4.设置背景颜色
glClearColor(0.8, 0.8, 0.4, 1.0);
}
注意:
1、上下文可以创建多个,但是一次只可以使用一个,需要使用[EAGLContext setCurrentContext:context];来设置当前的上下文环境
2、GLKView视图需要配置缓存区格式,这里配置了颜色缓存区和深度缓存区,这两个是必须配置的,可以作为GLKView初始化的一部分。
3、此处颜色的设置使用了glClearColor,其实当然也可以直接用background来设置。
重要代码:
设置渲染缓存区的格式
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
view.drawableDepthFormat = GLKViewDrawableDepthFormat16;
4.3.3 加载顶点数据、纹理数据
- 设置顶点数据
- 开辟顶点缓存区,并赋值数据到顶点缓存区中
- 打开读取数据通道并读取数据
-(void)setUpVertexData
{
//1.设置顶点数组(顶点坐标,纹理坐标)
/*
纹理坐标系取值范围[0,1];原点是左下角(0,0);
故而(0,0)是纹理图像的左下角, 点(1,1)是右上角.
*/
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, //左下
};
/*
顶点数组: 开发者可以选择设定函数指针,在调用绘制方法的时候,直接由内存传入顶点数据,也就是说这部分数据之前是存储在内存当中的,被称为顶点数组
顶点缓存区: 性能更高的做法是,提前分配一块显存,将顶点数据预先传入到显存当中。这部分的显存,就被称为顶点缓冲区
*/
//2.开辟顶点缓存区
//(1).创建顶点缓存区标识符ID
GLuint bufferID;
glGenBuffers(1, &bufferID);
//(2).绑定顶点缓存区.(明确作用,此处是用作数组缓存的)
glBindBuffer(GL_ARRAY_BUFFER, bufferID);
//(3).将顶点数组的数据copy到顶点缓存区中(GPU显存中)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
//3.打开读取通道.
/*
(1)在iOS中, 默认情况下,出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的.
意味着,顶点数据在着色器端(服务端)是不可用的. 即使你已经使用glBufferData方法,将顶点数据从内存拷贝到顶点缓存区中(GPU显存中).
所以, 必须由glEnableVertexAttribArray 方法打开通道.指定访问属性.才能让顶点着色器能够访问到从CPU复制到GPU的数据.
注意: 数据在GPU端是否可见,即,着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU(服务器端)数据。
(2)方法简介
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
*/
//顶点坐标数据
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(GLfloat) * 5, (GLfloat *)NULL + 3);
}
注意:
1、我们是将一张图贴到一个长方形上,所以是两个三角形,至于三角形的顶点坐标和纹理坐标的创建以及对应关系其实不是我们所需要深入研究的部分,所以略过。我们的重点在于获取到数据后的处理,至于数据的设计不是我们所做的事情。
2、顶点缓存区在显存中,GPU对其数据获取更快,所以需要将顶点数据从内存中的顶点数据拷贝到顶点缓存区中。
3、在读取顶点/纹理数据前需要先打开读取通道。之后添加相应的数据。
重要API:
开辟顶点缓存区的设置(重点记忆)
- glGenBuffers:获取缓存区标识符ID
- 之glBindBuffer:绑定顶点缓存区,这里的绑定是将这个缓存区绑定作为存储数组缓存的,所以必须要写GL_ARRAY_BUFFER。
- glBufferData:拷贝数据进入到该缓存区中,第一个参数是该缓存区的类型,第二个参数是缓存区的大小,第三个参数是顶点数据、第四个表示数据将送到GPU中绘制
//(1).创建顶点缓存区标识符ID
GLuint bufferID;
glGenBuffers(1, &bufferID);
//(2).绑定顶点缓存区.(明确作用,此处是用作数组缓存的)
glBindBuffer(GL_ARRAY_BUFFER, bufferID);
//(3).将顶点数组的数据copy到顶点缓存区中(GPU显存中)
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
打开通道(重点记忆)
在iOS中,默认情况下,所有的顶点着色器的属性变量是关闭的(注意顶点着色器和属性,其他的不用考虑这个),所以默认情况下顶点数据在着色器端是不可用的。
因此就算前面我们将数据从内存拷贝到显存,着色器也无法获取到数据,所以需要打开通道才能访问属性。
//顶点坐标数据
glEnableVertexAttribArray(GLKVertexAttribPosition);
//纹理坐标数据
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
//参数类型
typedef NS_ENUM(GLint, GLKVertexAttrib)
{
GLKVertexAttribPosition,//顶点坐标
GLKVertexAttribNormal,//法线坐标
GLKVertexAttribColor,//颜色
GLKVertexAttribTexCoord0,//纹理坐标
GLKVertexAttribTexCoord1//纹理坐标
} NS_ENUM_AVAILABLE(10_8, 5_0);
读取数据的方式(重点记忆)
我们这个数组是一维数组(OpenGL ES中推荐一维数组),包含有顶点坐标和纹理坐标,那么我们在显存中读取数据时,就需要设置读取方式,也就是设置读取哪部分数据,不能读错了。
至于API的介绍,注释足够详细,就不多说了。
(2)方法简介
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
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 5, (GLfloat *)NULL + 0);
4.3.4 使用GLBaseEffect加载纹理数据
- 取纹理图片路径
- 设置纹理参数
- 设置GLKBaseEffect
代码:
-(void)setUpTexture
{
//1.获取纹理图片路径
NSString *filePath = [[NSBundle mainBundle]pathForResource:@"cat" ofType:@"png"];
//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;
}
注意:
1、我们可以设置纹理参数,这里是设置了纹理坐标的显示位置,还有其他的也可以设置,具体的纹理参数可以参考OpenGL的纹理参数。
2、我们使用效果GLKBaseEffect来对纹理进行处理,这里只是将其纹理添加进入,并没有做任何操作,因为GLKBaseEffect内部会帮我们处理,我们只要传入即可,相当于OpenGL中的固定着色器。
重要API:
纹理参数的设置:
GLKTextureInfo是纹理信息对象,包含纹理及其属性。纹理属性可以通过一个字典传入设置。
纹理信息对象传递给效果,效果即可对该纹理进行处理。
//纹理坐标原点是左下角,但是图片显示原点应该是左上角.
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil];
效果的初始化:
效果类似于OpenGL中的固定着色器,无需我们自己设置顶点/片元着色器,只需要我们传入对应的顶点/纹理坐标,纹理数据,就会自动帮我们实现自己已定义好的特定效果。
enabled和name都是必须写的,表示可用和指定纹理信息
//3.使用苹果GLKit 提供GLKBaseEffect 完成着色器工作(顶点/片元)
cEffect = [[GLKBaseEffect alloc]init];
cEffect.texture2d0.enabled = GL_TRUE;
cEffect.texture2d0.name = textureInfo.name;
4.3.5 渲染
对效果进行绘制
代码:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
//1.清除缓存区
glClear(GL_COLOR_BUFFER_BIT);
//2.准备绘制
[cEffect prepareToDraw];
//3.开始绘制
//三角形、起始为0,总共6个
glDrawArrays(GL_TRIANGLES, 0, 6);
}
注:
1、OpenGL ES中清除缓存区还是必须要有的,要不然如果有存留会造成影响
2、先将该效果准备绘制,之后绘制时就会将这个效果渲染上屏
重要API:
准备绘制:[cEffect prepareToDraw];
绘制:glDrawArrays(GL_TRIANGLES, 0, 6);
4.4 总结
- GLKView的使用过程
- GLKView的效果的使用过程
- 重要API的掌握
GLKView的使用过程:
- 添加上下文环境
- 配置渲染缓存区的格式
GLKView的效果的使用过程:
- 将数据从内存拷贝到显存
- 打开读取通道并设置读取数据方式
- 设置纹理参数
- 进行渲染
重要API:
- 上下文环境的创建并设置
- 缓存区格式的设置
- 顶点数据从内存拷贝到显存中
- 打开读取通道并设置读取数据方式
- 设置纹理参数