初识OpenGL ES
简介
OpenGL ES (OpenGL for Embedded Systems) 是以手持和嵌入式为目标的⾼级3D图形应 ⽤程序编程接口(API)。OpenGL ES 是⽬前智能手机中占据统治地位的图形API,其作用范围已经扩展到桌面。OpenGL ES支持的平台包括iOS, Andriod , BlackBerry ,bada ,Linux 和Windows,它还是基于浏览器的3D图形Web标准WebGL的基础。
OpenGL ES是OpenGL的简化版,其是为创建适合于受限设备(手持设备和嵌入式设备等)使用的API而产生的。
OpenGL ES的渲染流程
OpenGL ES 2.0规范之后采用了可编程图形管线。下面概述OpenGL ES图形管线的各个阶段,其中灰色部分为可编程阶段。(一些教程中又将片段着色器称为片元着色器)
图形管线缓存
在上图的图形管线中可分为两部分,API部分为客户机,其余部分为服务器。在图形管线的整个流程中,OpenGL ES横跨在GPU和CPU两个处理器之间,协调两个内存区域之间的数据交换。程序会保存3D场景数据到硬件随机存取存储器(RAM)中。嵌入式系统的中央处理单元有专门为其分配的RAM。在图形处理的过程中,GPU也有专门为其分配的RAM。由于CUP和GPU的计算速度远远大于读写内存的速度,为了打破因为数据交换而产生的性能瓶颈,OpenGL ES抛弃了OpenGL中低效的内存复制操作,转而通过缓存(buffers)的方式来降低数据交换带来的影响。
缓存是指图形处理器能够控制和管理的连续RAM。程序从CPU的内存复制数据到OpenGL ES的缓存,在GPU去的一个缓存的所有权后,运行在CPU中的程序理想情况下将不再接触这个缓存。通过独占的缓存,GPU就能够尽可能以最有效的方式读写内存。为缓存提供数据又如下7个步骤:
- 1、生成glGenBuffers()——请求OpenGL ES为图形处理器控制的缓存生成一个独一无二的标识符。
- 2、绑定glBindBuffer()——告诉OpenGL ES为接下来的运算使用一个缓存。
- 3、缓冲数据glBufferData()或glBufferSubData()——让OpenGL ES为当前绑定的缓存分配并初始化足够的连续内存(通常是从CPU控制的内存复制数据到分配的内存)。
- 4、启用glEnableVertexAttribArray()或禁止glDisVertexAttribArray()——告诉OpenGL ES在接下来的渲染中是否使用缓存中的数据。
- 5、设置指针glVertexAttribPointer()——告诉OpenGL ES在缓存中的数据的类型和所有需要访问的数据的内存偏移值。
- 6、绘制glDrawArrays()或glDrawElements()——告诉OpenGL ES使用当前绑定并启用的缓冲中的数据渲染整个场景或者某个场景的一部分。
- 7、删除glDeleteBuffers()——告诉OpenGL ES删除以前生成的缓存并释放相关的资源。
生成、初始化、和删除缓存有时需要消耗时间来同步图形处理器和CPU,存在这个延迟是因为GPU在删除一个缓存之前必须完所有与该缓存相关的等待中的运算。
硬件和OpenGL ES之间的关系.png帧缓存
GPU需要知道应该在内存中的哪个位置存储渲染出来的2D图像像素数据。就像GPU提供数据的缓存一样,接收渲染结果的缓冲区叫做帧缓存(frame buffer)。程序会像任何其它种类的缓存一样生成、绑定、删除帧缓存。但是帧缓存不需要初始化,因为渲染指令会在适当的时候交替缓存的内容。帧缓存会在绑定时隐式开启,同时OpenGL ES会自动地根据平台硬件配置和功能来设置数据的类型和偏移。帧缓存可以同时存在很多个,并且可以通过OpenGL ES让GPU把渲染结果存储到任意数量的帧缓存中。屏幕显示像素受到保存在前帧缓存(front frame buffer)的特定帧缓存中的像素颜色元素控制。程序和操作系统很少会直接渲染到前帧缓存中,因为那样会让用户看到正在渲染中还诶与渲染完成的图像。相反,会把渲染结果保存到包括后帧缓存(back frame buffer)在内的其他帧缓存中。当渲染后的后帧缓存包含一个完成的图像时,前、后帧缓存几乎会瞬间切换。后帧缓存变成新的前帧缓存,同事旧的前帧缓存会变成后帧缓存。
OpenGL ES的上下文
OpenGL ES是一个状态机器,即在一个程序中设置了一个配置值只有,这个值会一直保持,知道程序修改了这个值。用于配置OpenGL ES的信息会被封装到一个上下文(context)中,上下文中的信息可能会被保存在CPU所控制的内存中,也可能被保存在GPU所控制的内存中。OpenGL ES会按需在两个内存区域间复制信息。
顶点着色器
顶点着色器的输入包括:
- 着色器程序(Shader Program) —— 描述顶点上执行操作的顶点着色器程序源代码或者可执行文件
- 顶点着色器输入(或者属性) —— 用顶点数组提供的每个顶点的数据
- 统一变量(uniform) —— 顶点(或片段)着色器使用的不变数据
- 采样器 —— 代表顶点着色器使用纹理的特殊统一变量类型
顶点着色器的输出在OpenGL ES 2.0中称作可变(varying)变量,但是在OpenGL ES 3.0中改名为顶点着色器输出变量。在图圆光栅化阶段,为每个生成的片元计算顶点着色器输出值,并作为输入传递给片元着色器。用于从分配给每个片元顶点的顶点着色器输出生成每个片段值的机制称为插值(Interpolation)。顶点着色器可以用于通过矩阵变换位置、计算照明公式来生成逐顶点颜色以及生成或者变换纹理坐标等基于顶点的传统操作。
顶点着色器.png图元装配
顶点着色器之后,OpenGL ES 3.0图形管线的下一阶段是图元装配。图元(Primitive)是三角形、直线或者店精灵等集合对象。图元的每个顶点被发送到顶点着色器的不同拷贝。在图元装配期间,这些顶点被组合成图元。
对于每个图元必须确定图元是否位于视锥体内,如果图元没有完全在视锥体内,则可能需要进行裁剪。如果图元完全处于该区域之外,它会被抛弃。裁剪之后,顶点位置被转换为屏幕坐标。也可以执行一次淘汰操作,根据图元面向前方或者后放抛弃他们(深度测试)。裁剪和淘汰之后,图元便准备传递给管线的下一阶段——光栅化阶段。
光栅化
在光栅化阶段需要绘制对应的图元(点精灵、直线或者三角形)。光栅化是将图元转化为一组二维片段的过程,然后这些片段由片元着色器处理。这些二维片段代表着可在屏幕上绘制的像素。
光栅化.png片元着色器(片段着色器)
片元着色器为片元上的操作实现了通用的可编程方法。它可以用于图片/视频/图形中每个像素的颜色填充(像视频/图片添加滤镜实际上就是将视频中每个图片的像素点颜色填充进行修改)。如图1-4所示, 对光栅化阶段生成的每个片元执行这个着色器,输入方式如下:
- 着色器程序 ——描述片段上所执行操作的片段着色器程序源代码或者可执行文件。
- 输入变量——光栅化单元用插值为每个片元生成的顶点着色器输出。
- 统一变量——片段(或者顶点)着色器使用的不变数据。
- 采样器——代表片元着色器所用纹理的特殊统一变量类型。
片元着色器可以抛弃片元,也可以生成一个或者多个颜色值作为输出。一般来说,除了渲染到多重渲染目标之外,片元着色器只输出一个颜色值;在多重渲染目标的情况下,为每个渲染目标输出一个颜色值。
由于参考资料不同,片元着色器和片段着色器是同一概念。懒得改了
逐片段操作
在片段着色器之后,下一阶段是逐片段操作。光栅化生成的屏幕坐标为(xw, yw) 的片段只能修改帧缓冲区中位置为(xw, yw)的像素。图1-5描述了OpenGL ES 3.0逐片段操作阶段。
逐片操作.png在逐片段操作阶段,在每个片段阶段上执行如下功能:
- 像素归属测试——确定帧缓冲区中位置(xw, yw)的像素目前是不是归OpenGL ES所有。该测试使窗口系统能够控制帧缓冲区中的哪些像素属于当前OpenGL ES上下文。
- 裁剪测试——确定位置(xw, yw)是否位于作为OpenGL ES状态的一部分的裁剪矩形范围内。若该片段位于裁剪区域之外,则被抛弃。
- 模板和深度测试——这些测试在输入片段的模板和深度值上进行,以确定片段是否应该被拒绝。
- 混合——混合将新生成的片段颜色值与保存在帧缓冲区(xw, yw)位置的颜色值组合起来。
- 抖动——抖动用于最小化因使用有限精度在帧缓冲区中保存颜色值而产生的伪像。
下面是通过OpenGL ES 2.0绘制三角形的代码,便于理解记忆:
.h
#import <GLKit/GLKit.h>
@interface ViewController : GLKViewController
@end
.m
#import "ViewController.h"
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
typedef struct {
GLKVector3 positionCoords;
}SceneVertex;
static const SceneVertex vertices[] = {
{{-0.5f, -0.5f, 0.0}},
{{0.5f, -0.5f, 0.0}},
{{0.5f, 0.5f, 0.0}}
};
static const GLfloat points[] = {
-0.5f, -0.5f, 0.0,
0.5f, -0.5f, 0.0,
0.5f, 0.5f, 0.0
};
@interface ViewController (){
GLKBaseEffect * baseEffect;
GLuint vertexBufferID;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpConfig];
// 记得将storyboard中的view设置为GLKView
}
- (void)setUpConfig
{
// EAGLContext 上下文,用于配置OpenGL ES在特定平台的软件数据结构中的信息
EAGLContext * context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
GLKView * view = (GLKView *)self.view;
view.context = context;
[EAGLContext setCurrentContext:context];
baseEffect = [[GLKBaseEffect alloc] init];
baseEffect.useConstantColor = GL_TRUE;
baseEffect.constantColor = GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f);
glClearColor(0.2f, 0.7f, 0.4f, 1.0f);
//该函数的第一个参数用于指定要生成的缓存标识符的数量,第二个指针参数指向生成的标识符的内存保存位
置,当前情况下一个标识符被生成,保存在vertexBufferID实例变量中
glGenBuffers(1, &vertexBufferID);
/**
该函数绑定用于指定标识符的缓存到当前缓存。在任意时刻每种类型只能绑定一个缓存.
参1是一个常量,用于指定要绑定哪一种类型的缓存。该函数的实现只支持两种缓存类型,
GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER, GL_ARRAY_BUFFER类型用于指定
一个顶点属性数组
参2是要绑定的缓存的标识符
*/
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID);
/**
该函数复制应用的顶点数据到当前上下文所绑定的顶点缓存中
参1:用于指定要更新当前上下文中所绑定的是哪一个(种)缓存
参2:指定要复制进这个缓存的字节数量
参3:要复制的字节的地址
参4:提示缓存在未来的运算中可能将会被怎么使用。GL_STATIC_DRAW提示会告诉上下文,
缓存中的内容适合复制到GPU控制的内存,因为很少对其进行修改,可以帮助GL优化内存使用。
使用GL_DYNAMIC_DRAW会告诉上下文,缓存内的数据会频繁的改变,提示GL以不同的方式来处
理缓存中的存储
*/
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
// glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
}
#pragma mark -- GLKViewDelegate
/**
每当一个GLKView实例需要被重绘时,它都会让保存在视图的上下文属性中的GL的上下文成为当前上下文。
GLKView实例会绑定与一个Core Animation层分享的帧缓存,执行其他的GL配置,并发送一个消息来调用
glkView:drawInRect:方法。
*/
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
[baseEffect prepareToDraw];
//该函数来设置当前绑定的帧缓存的像素颜色渲染缓存中的每一个像素的颜色为当前使用glClearColor()
函数设定的值,帧缓存可能有除了像素颜色渲染缓存之外的其它附加缓存,并且如果其它缓存被使用了,它们可以
通过glClear函数中指定不同的参数来清除。glClear函数会有效地设置帧缓存中的每一个像素的颜色为背景色
glClear(GL_COLOR_BUFFER_BIT);
//出于性能考虑,所有顶点着色器的属性(Attribute)变量都是关闭的,
意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU,由glEnableVertexAttribArray
启用指定属性,才可在顶点着色器中访问逐顶点的属性数据。glVertexAttribPointer或VBO只是建立
CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即,着色器能
否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着
色器读取GPU(服务器端)数据。
glEnableVertexAttribArray(GLKVertexAttribPosition);
/**
该函数告诉OpenGL ES顶点数据在哪里,以及怎么解释为每个顶点保存的数据
参1:只是当前绑定的缓存包含每个顶点的位置信息
参2:只是每个位置有3个部分
参3:告诉GL每个部分都保存为一个浮点类型的值
参4:高速GL小数点固定数据是否可以被改变
参5:该参数叫做“步幅”,它指定了每个顶点的保存需要多少个字节,即指定了GPU从一个顶点的内存开
始位置,转到下一个顶点的内存开始位置需要跳过多少字节
*/
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE,
sizeof(GLfloat) * 3, (GLfloat *)NULL);
// 通过顶点结构体绘制,需要复合顶点数组的数据
// glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT,GL_FALSE,
sizeof(SceneVertex), NULL);
/**
参1:设置绘制方式
参2:指定缓存内的需要渲染的第一个顶点的位置
参3:缓存内需要渲染的顶点的数量
*/
glDrawArrays(GL_TRIANGLES, 0, 3);
}
@end
// VBO: vertex buffer object 顶点缓冲区
// VAO: Vertex Array Object 顶点数组对象
// IBO: Index Buffer Object 顶点索引数组