OpenGL ES:新手村中的HelloWorld
前言
对于OpenGL ES,本人现在还是一个小白,所以我将用小白视角对OpenGL ES进行小白式的讲解.希望能通过如此帮助更多的人.同时我要感谢一个人,那就是落影loyinglin,落影大神关于OpenGL ES方面的知识写的非常的详细,大家可以去参考.好了,开始战斗吧.
OpenGL ES简介
OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。该API由Khronos集团定义推广,Khronos是一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准。
OpenGL ES 是从 OpenGL 裁剪的定制而来的,去除了glBegin/glEnd,四边形(GL_QUADS)、多边形(GL_POLYGONS)等复杂图元等许多非绝对必要的特性。经过多年发展,现在主要有两个版本,OpenGL ES 1.x 针对固定管线硬件的,OpenGL ES 2.x 针对可编程管线硬件。OpenGL ES 1.0 是以 OpenGL 1.3 规范为基础的,OpenGL ES 1.1 是以 OpenGL 1.5 规范为基础的,它们分别又支持 common 和 common lite两种profile。lite profile只支持定点实数,而common profile既支持定点数又支持浮点数。 OpenGL ES 2.0 则是参照 OpenGL 2.0 规范定义的,common profile发布于2005-8,引入了对可编程管线的支持。
那么上面说了这么一些到底是什么意思呢.其实就是说OpenGL ES是移动端处理图像的一个C语言库.
OpenGL ES的显示图像
在iOS中,我们平常要加载一张图片会怎么做呢?一个是使用UIKit框架的UIImage,一个是使用Core Graphics框架直接绘制.那么OpenGL ES是如何展现图像的呢?今天我们就先用OpenGL ES中的GLKBaseEffect来展现图像.实现效果如下所示.
HelloWorld的实现过程
一、 准备工作
</b>
为了简便省时,我决定直接在ViewController中修改代码,首先我们先导入GLKit.h头文件,紧接着那个将ViewController的类型修改为GLKViewController.
然后在Main.storyboard修改ViewController中view的类型为GLKView.如图所示.
上面的准备工作已经是做完了,那么接下来,就是正题部分了,我们现在ViewController.m中声明两个属性.一个是OpenGL ES 上下文属性的EAGLContext对象,一个是矩阵相关的GLKBaseEffect对象.
@interface ViewController ()
@property(nonatomic,strong)EAGLContext *mContext;
@property(nonatomic,strong)GLKBaseEffect *mEffect;
@end
通过官方的API文档,我们知道,EAGLContext对象管理一个OpenGL ES渲染环境状态信息,命令,以及使用OpenGL ES的所需要资源。OpenGL ES执行任何命令之前,都需要通过EAGLContext对象来实现。同时官方文档也提到,绘制一个上下文之前,你必须完成framebuffer对象绑定到上下文。
GLKBaseEffect这个类实现了OpenGL ES 1.0公共(common)的shading行为,简化(从1.0)到OpenGL ES 2.0的转化。它们也提供了让光和纹理(lighting and texturing)工作的简单方法。GLKBaseEffect对象提供了着色器这一功能.其实着色器应该算的上是OpenGL ES的一大特色,但是GLKBaseEffect已经包含了这一功能,相当于封装了着色器.现在只是知道有着色器就行.
</b>
接下来,我们了解两个方法,他们的刷新频率和屏幕的刷新频率是一致的.我们需要在- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
这个方法中进行渲染操作.
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect;
-(void)update;
</b>
二、ViewDidLoad的配置工作
</b>
我们接下来在ViewDidLoad中需要做以下几个工作.
1、配置OpenGL ES 上下文信息
self.mContext = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
GLKView *view = (GLKView *)self.view;
view.context = self.mContext;
//颜色缓冲区格式
view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
[EAGLContext setCurrentContext:self.mContext];
代码讲解:第一行代码是对控制器自身的EAGLContext对象使用- (instancetype) initWithAPI:(EAGLRenderingAPI) api;
进行初始化.EAGLRenderingAPI是一个枚举类型.包含了三个值,分别代表着1.0、2.0和3.0的OpenGL ES,我们现在使用的OpenGL ES2.0,所以选择的是kEAGLRenderingAPIOpenGLES2
;
typedef NS_ENUM(NSUInteger, EAGLRenderingAPI)
{
kEAGLRenderingAPIOpenGLES1 = 1,
kEAGLRenderingAPIOpenGLES2 = 2,
kEAGLRenderingAPIOpenGLES3 = 3,
};
第二行和第三行代码则是把当前控制器的View的context设置为self.mContext.
第四行代码则是设置页面的颜色缓冲区格式,默认的也是GLKViewDrawableColorFormatRGBA8888
,所以不做设置也可以.
第五行代码则是将此“EAGLContext”实例设置为OpenGL的“当前激活”的“Context”。这样,以后所有“GL”的指令均作用在这个“Context”上。
2、配置绘制矩阵数组信息
OpenGL ES的坐标系是和iOS常用的Quartz 2D坐标系是不一样的.OpenGL ES是左手坐标系(待议~),Quartz 2D坐标系则是右手坐标系.OpenGL ES的坐标系是以中心为原点,原点到屏幕的边缘为单位1(不管屏幕尺寸如何变化,都是单位1).OpenGL ES的坐标系如下所示.
OpenGL ES的坐标系接下来,我们创建顶点数组,数组中的元素类型为GLfloat类型.数组中包含了两个坐标一个是顶点坐标(x,y,z轴信息),一个是纹理坐标(x,y信息),注意,顶点要与纹理的点一一对应.关于纹理相关只是可以参考我在SpriteKit的文集中的SpriteKit框架之SKTextureAtlas第一部分内容.代码如下所示.
(PS:问什么要这么创建数组呢?难道是系统规定的?回答:并不是,数组的形式规则定好之后,我们可以按照一定的规律取出对应的数据元素.然后进行操作.)
//顶点数据,前三个是顶点坐标,后面两个是纹理坐标
GLfloat squareVertexData[] =
{
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, //左下
0.5, 0.5, -0.0f, 1.0f, 1.0f, //右上
};
上面的顶点坐标组成看似是一个正方形,但是实际上真的如此吗?NONONO,事实上,由于屏幕的宽高不相同的原因.所选择区域会是下面的这个样子的.
</b>
对于初学者还有个容易忽视的技术点,那就是 在OpenGL ES只能绘制三角形,不能绘制多边形,但是在OpenGL中确实可以直接绘制多边形.
那么我们改如何绘制一个矩形呢?我们可以认为一个矩形是两个三角形拼接而成的.这时候,我们就需要整出另外一个东西:那就是顶点索引数组.有了它就可以规定绘制的顺序.如下所示.
//顶点索引
GLuint indices[] ={
0, 1, 2,
1, 3, 0
};
绘制过程如图所示.先是做下三角形,再是右上三角形.
绘制顺序图示
3.将顶点数据和顶点索引数据写入通用的顶点属性存储区 (重点核心部分❗️❗️❗️)
其实我一直没有理解"通用"这个词(写这篇博客写到最后竟然理解了,因为顶点属性集中包含五种属性:位置、法线、颜色、纹理0,纹理1,所以只能用"通用"这个词了).那么把顶点数据和顶点索引数据写入通用的顶点属性存储区,是怎么样的过程呢?首先将顶点数据和顶点索引数据保存进GUP的一个缓冲区中,然后再按一定规则,将数据取出,复制到各个通用顶点属性中.
那么接下来,我们先将顶点数组保存进GPU缓冲区中.代码如下所示.
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(squareVertexData), squareVertexData, GL_STATIC_DRAW);
然后就是把顶点索引数组写进GPU缓冲区中.
GLuint index;
glGenBuffers(1, &index);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
函数说明:
-
glGenBuffers(GLsizei n,GLuint *buffers)
:任何非零的无符合整数都可以作为缓冲区对象的标识符使用。这个函数的作用就是向系统申请n个缓冲区,系统把这n个缓冲区的标识符都放进buffers数组中。还可以调用glIsBuffer()函数判断一个标识符是否正被使用。
例如,glGenBuffers(1, &index);
这是是向系统申请1个缓冲区,标识符为index. -
glBindBuffer(GLenum target, GLuint buffer)
:把这个缓冲区绑定给顶点还是索引.通俗点,也就是定义了这个缓冲区存储的是什么.target用于决定绑定的是顶点数据(GL_ARRAY_BUFFER)还是索引数据(GL_ELEMENT_ARRAY_BUFFER). -
glBufferData (GLenum target, GLsizeiptr size, const GLvoid* data, GLenum usage)
:把CPU中的内存中的数组复制到GPU的内存中.target用于决定绑定的是顶点数据(GL_ARRAY_BUFFER)还是索引数据(GL_ELEMENT_ARRAY_BUFFER).size决定数据的存储长度.data则是数据信息.usage表示数据的读写方式,是一个枚举类型,这里使用的是GL_STATIC_DRAW
,它表示此缓冲区内容只能被修改一次,但可以无限次读取。
</b>
然后,将GPU缓冲区的顶点数据复制进通用顶点属性中.注意:索引数据不需要复制到通用顶点属性中.具体代码如下.
//顶点数据
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);
函数说明:
-
glEnableVertexAttribArray (GLuint index)
: 激活顶点属性(默认它的关闭的).在刚开始,我们就说到顶点属性集中包含五种属性:位置、法线、颜色、纹理0,纹理1.顶点属性集是一个枚举值.这里我们只用到了位置和纹理这两个属性.
typedef NS_ENUM(GLint, GLKVertexAttrib)
{
GLKVertexAttribPosition,
GLKVertexAttribNormal,
GLKVertexAttribColor,
GLKVertexAttribTexCoord0,
GLKVertexAttribTexCoord1
} NS_ENUM_AVAILABLE(10_8, 5_0);
</b>
-
glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
: 往对应的顶点属性中添加数据.indx为顶点属性类型.size为每个数据中的数据长度.type为元素数据类型,normalized填充时需不需要单位化.stride需要填写的是在数据数组中每行的跨度,最后一个ptr指针是说的是每一个数据的起始位置将从内存数据块的什么地方开始。例如顶点属性的数据填充示意图如下所示.
4.将图片纹理赋值给GLKBaseEffect对象
本文的前面我就提到了图片纹理和纹理集,纹理集最好的好处就是节省内存,具体看我以前写的博客,上面有提到,这里就不啰嗦了.在OpenGL ES也是有纹理(GLKTextureInfo)这一概念,应该说Sprite Kit框架就是封装的OpenGL ES😆.下面我们先把图片加载到纹理对象中.
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"jpg"];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:@(1),GLKTextureLoaderOriginBottomLeft, nil];
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithContentsOfFile:filePath options:options error:nil ];
然后创建GLKBaseEffect对象并且开启它的可编辑状态,然后把纹理赋值给GLKBaseEffect对象.
self.mEffect = [[GLKBaseEffect alloc]init];
self.mEffect.texture2d0.enabled = GL_TRUE;
self.mEffect.texture2d0.name = textureInfo.name;
</b>
三、渲染场景
</b>
可能没接触过Sprite Kit的童靴不太了解场景(Scene),你可以理解为是绘制图层,当然了,这个绘制的频率是跟屏幕刷新频率是一致的(默认的).在GLKit框架中,GLKView对象是完全不需要做任何操作的,只要在控制器中执行- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
和-(void)update
这两个方法就可以在GLKView对象上显示图像了.一般我们把渲染代码写在- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
.如下所示.
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glClearColor(0.3f, 0.6f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//启动着色器
[self.mEffect prepareToDraw];
glDrawElements(GL_TRIANGLES, self.mCount, GL_UNSIGNED_INT, 0);
}
函数说明:
-
glClearColor (GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha)
: 渲染前的“清除”操作,指定在清除屏幕之后填充什么样的颜色.四个参数就是RGB值. -
glClear (GLbitfield mask)
:指定需要清除的缓冲.mask指定缓冲的类型.可以使用 | 运算符组合不同的缓冲标志位,表明需要清除的缓冲.可以使用以下标识符.
GL_COLOR_BUFFER_BIT: 当前可写的颜色缓冲
GL_DEPTH_BUFFER_BIT: 深度缓冲
GL_ACCUM_BUFFER_BIT: 累积缓冲
GL_STENCIL_BUFFER_BIT: 模板缓冲
[self.mEffect prepareToDraw];
这个就是启动当前GLKBaseEffect对象.
-
glDrawElements (GLenum mode, GLsizei count, GLenum type, const GLvoid* indices)
: 通过顶点索引绘制图像.mode指定的绘制的类型.类型展示如下,这里使用的是GL_TRIANGLES
,count指定的顶点索引数组中元素的个数,type 为索引数组(indices)中元素的类型,只能是下列值之一:GL_UNSIGNED_BYTE
,GL_UNSIGNED_SHORT
,GL_UNSIGNED_INT
. indices指向索引数组的指针。
GL_POINTS: 单独的将顶点画出来。
GL_LINES: 单独地将直线画出来。
GL_LINE_STRIP: 连贯地将直线画出来。
GL_LINE_LOOP: 连贯地将直线画出来。行为和GL_LINE_STRIP类似,但是会自动将最后一个顶点和第一个顶点通过直线连接起来。
GL_TRIANGLES:这个参数意味着OpenGL使用三个顶点来组成图形。所以,在开始的三个顶点,将用顶点1,顶点2,顶点3来组成一个三角形。完成后,在用下一组的三个顶点(顶点4,5,6)来组成三角形,直到数组结束。
GL_TRIANGLE_STRIP: OpenGL的使用将最开始的两个顶点出发,然后遍历每个顶点,这些顶点将使用前2个顶点一起组成一个三角形。
GL_TRIANGLE_FAN: 在跳过开始的2个顶点,然后遍历每个顶点,让OpenGL将这些顶点于它们前一个,以及数组的第一个顶点一起组成一个三角形。
HelloWorld之路的结束
如果没有太大的问题的话,那么我们就会出现一开始的模拟器效果了.想想一张图片展示底层显示代码是这么的多,想哭有木有😭.其实这只是OpenGL ES的冰山一角.接下来,我将对着色器相关的部分进行研究讲解,如果有任何疑问,可以在评论区回复,共同讨论进步.最后附上HelloWorld的实现Demo.