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.

OpenGLES相关学习Demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,248评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,681评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,443评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,475评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,458评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,185评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,451评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,112评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,609评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,083评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,163评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,803评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,357评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,357评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,590评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,636评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,925评论 2 344

推荐阅读更多精彩内容