iOS OpenGL ES 3 编程 2:绘制三角形、屏幕旋转与架构设计

1、OpenGL ES的版本区别

由于OpenGL ES 2.0及以上版本都改为可编程管线,ES 1.0是固定功能管线,它们之间的编程模式区别较大。可以认为对同一问题的处理,ES 2.0、3.0等更底层、可操作空间更大,缺点是,实现同一功能需要更多代码,增大了开发难度。而且,这些OpenGL ES版本并不是相互取代关系,而是有不同的侧重点。ES 1.0消耗资源少,倾向二维图形处理,三维图形处理能力较弱。ES 2.0开始加强了三维图形的处理能力,当然消耗的资源也随之增加,提高了对硬件设备的要求,同时编程模式与ES 1.0区别较大,具体操作基本在着色器(Shader)中完成,不向下兼容导致ES 1.0很多函数在2.0和3.0中被删除。ES 2.0、3.0的一般处理流程为顶点数据达到顶点着色器(Vertex Shader),经顶点着色器对坐标进行变换处理,进入图元装配(Primitive Assembly)形成指定绘制的图形,接着进入片段着色器(Fragment Shader)进行像素的颜色处理,随后开始光栅化(Rasterization)等等操作,如下图所示。可编程管线具体表现为,向开发者开放了两种着色器(桌面版OpenGL还有Compute Shader等),所有奇妙的功能都由着色器编程实现。

OpenGL ES程序管线图

着色器是一种语法类似C语言的小程序片段。不同的图形接口有不同的着色器语言,对于OpenGL,是GLSL(OpenGL Shader Language),Metal也有自己的着色器语言。就我经历而言,Metal编程模型更直观,OpenGL过于古老,不好理解。

2、OpenGL ES 3.0绘制三角形

在开始具体操作前,先认识下OpenGL ES程序的运行流程。ES采用服务器/客户端编程模型,CPU是客户端,所调用的函数发送至GPU(服务器端),被GPU转换成底层图形硬件支持的绘制命令。

OpenGL is designed to translate function calls into graphics commands that can be sent to underlying graphics hardware.  Because this underlying hardware is dedicated to processing graphics commands, OpenGL drawing is typically very fast.

程序运行流程

iOS OpenGL ES 3 编程 1:"Hello world"可知,绘制三角形的操作应该在清除缓冲区操作之后执行。前面描述了具体操作由着色器实现,那么,让我们来认识下着色器吧。

2.1、着色器(Shader)

着色器在Xcode中并不会被编译,而是以源码字符串形式存在,等App运行期间,由ARM(在iOS上)处理器运行期间编译成当前图形硬件兼容的可执行文件,过程类似C语言程序的编译链接过程。虽然OpenGL ES标准提供了加载已编译的着色器二进制数据,但是iOS不支持这种做法,有关此问题后续再展开描述。

1、顶点着色器

Vertices are transformed and lit, assembled into primitives, and rasterized to create a 2D image.

#version300eslayout(location =0) in vec4 position;voidmain(){    gl_Position = position;}

OpenGL ES 3.0的着色器编写比2.0多了一项要求:在开头声明版本信息,#version 300 es,300表示使用OpenGL ES 3.0。改成310则表示OpenGL ES 3.1,Nexus 6P支持3.1,所有iOS设备目前最高只支持3.0。由于3.0向下兼容2.0,意味着2.0语法编写的着色器也能正常使用。

in表示输入参数,vec4为类型,表示向量(x, y, z, w),类似的vec3则为向量(x, y, z),以此类推。position则为参数名,数据一般由CPU上传到GPU,可当作是CPU与GPU之间的通信端口。layout(location = 0)是指定属性索引为0,ES 3.0最多支持16个属性,默认按自然顺序递增排列,可用location修改它们的顺序,这也是后续CPU上传数据到GPU的依据。

ES 3.0有三种参数修饰符,in、uniform、out。其中,uniform和ES 2.0一样,表示不可变的数据,在顶点与片段着色器之间共享数据,每个顶点和片段着色器都可访问到同一数值,其余对应关系为:

in ==  attribute,表示输入数据

out == varying,表示输出数据,供渲染管线后续操作使用

gl_Position为GLSL内建变量,表示顶点坐标,数据类型为vec4。除此之外,还有几个内建变量,后续文档再介绍。

2、片段着色器

#version300esprecision highpfloat;out vec4 o_color;voidmain(){    o_color = vec4(1.0,1.0,0,1.0);// RGBA}

比顶点着色器多一个要求:若使用浮点数,则必须指定浮点数精度。精度越高,对应的颜色过渡更细腻,计算耗时越高,美丽的东西总是要付出更高的代价。由于ES 3.0不再提供gl_FragColor内建变量,当使用完全符合3.0语法的GLSL时,使用gl_FragColor导致编译错误。为表示顶点对应的像素颜色值,在此声明了一个vec4类型的变量o_color。

有关着色器内容的编码都完成了,下面介绍如何使用它们。

2.2、编译及使用着色器

前面提及了着色器是以源码字符串形式保存,且在App运行期间编译,那么,下面介绍编译着色器的步骤。

2.2.1、编译着色器

需要编译两种着色器:顶点(GL_VERTEX_SHADER)、片段(GL_FRAGMENT_SHADER)。着色器源码可能存在编写错误导致编译失败,故需要做编译检查,OpenGL ES不会主动提示编译结果,需要主动查询。

着色器的编译与编译C代码流程类似:

创建着色器

指定着色器源码

编译源码

检查编译错误

在合适的时候,删除已编译的着色器数据

示例代码如下:

GLuintcompileShader(char*shaderContent, GLenum shaderType){// 1GLuint shader = glCreateShader(shaderType);// 2glShaderSource(shader,1, &shaderContent,NULL);// 3glCompileShader(shader);// 4GLint compileStatus;    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);if(compileStatus == GL_FALSE) {        GLint infoLength;        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength);if(infoLength >0) {            GLchar *infoLog =malloc(sizeof(GLchar) * infoLength);            glGetShaderInfoLog(shader, infoLength,NULL, infoLog);printf("%s -> \n%s\n", C_STRING(shaderType), infoLog);free(infoLog);        }    }returnshader;}

有关错误输出,也可直接用字符串数组,省掉分配堆内存的麻烦。

GLint shaderCompileLogLength;glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &shaderCompileLogLength);char compileMessage[shaderCompileLogLength];glGetShaderInfoLog(shader, shaderCompileLogLength, NULL, compileMessage);printf("%s-> \n%s\n", C_STRING(shaderType), compileMessage);

删除编译器一般在释放绘制资源时进行,传递前面保存的着色器句柄给void glDeleteShader(GLuint shader);即可,此函数并不立即删除着色器,而是将指定着色器标志为删除,当着色器不与任何程序对象(program)关联(Attach)时才会被清理出内存。

2.2.2、使用着色器

着色器并不能单独作用于OpenGL,而是通过一个中介组织起来使用,这就是程序对象(program)。OpenGL ES规定一个program必须搭配一对着色器,且只能一对,即有效的progam = vertex shader + fragment shader。

等看到程序执行结果,很多人会有疑问,为何只指定了几个顶点及其颜色,图形却显现了过渡色彩。

The OpenGL ES specification does not define a windowing layer; instead, the hosting operating system must provide functions to create an OpenGL ES rendering context, which accepts commands, and a framebuffer, where the results of any drawing commands are written to. Working with OpenGL ES on iOS requires using iOS classes to set up and present a drawing surface and using platform-neutral API to render its contents.

3、OpenGL ES处理屏幕旋转

iPhone等设备的屏幕旋转会让前述小节所创建的图形超出屏幕范围,具体情况是,竖屏启动App再将屏幕横过来,或者反过来,如下所示。

竖屏转横屏出现偏移

横屏转竖屏出现偏移

显然,这都不是我们希望的结果,需要修复。

3.1、简单修复

[self.view addSubview:view];添加我们自定义的GLView作为子视图,在屏幕旋转时会出现上述偏移问题。一个简单的处理是,在Storyboard中将View的class设置成我们自定义的GLView或在Controller中令self.view = view;,这两个语句作用一样。

Storyboard设置这种方式要求我们覆盖initWithCoder:,而我们覆盖的是initWithFrame:,导致代码并不执行,还得将类似逻辑在initWithCoder:中实现才有效果,这样造成了代码冗余。

无论是Storyboard、Xib或initWithFrame:和self.view = view;,视图在显示时都会执行layoutSubviews,那么,在这个交集中绘制是个不错的选择。将initWithFrame:中的绘制代码迁移到layoutSubviews并删除View中其他代码,再运行App,可发现,在屏幕发生旋转时,画面正常。

但是,[self.view addSubview:view];的方式调用问题依旧。这需要ViewController通知View重新布局子视图才能触发layoutSubviews。就此问题作进一步分析。

首先,Controller覆盖- viewWillTransitionToSize: withTransitionCoordinator:。

-  (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator {NSLog(@"size = %@, layer rect = %@",NSStringFromCGSize(size),NSStringFromCGRect(self.view.layer.bounds));    [self.viewlayoutIfNeeded];}

size为旋转后屏幕大小,而view.layer.bounds为旋转前屏幕大小,且layoutIfNeeded并没令我们自定义的View执行layoutSubviews。

layoutIfNeeded无效

同样,[self.view setNeedsLayout];也不触发layoutSubviews。既然,我们的View是Controller的View的子视图,那么,遍历子视图逐一发送刷新通知会如何?

for(UIView*subviewinself.view.subviews) {    [subview layoutIfNeeded];}

执行发现,不触发layoutSubviews。改成[subview setNeedsLayout];,此时触发layoutSubviews,但是结果还是错误的。

遍历通知子视图作刷新

4、OpenGL ES 架构设计

OpenGL ES的接口基于C实现,可与Objective-C、Objective-C++、C++等语言无缝混合编程。在图形编程领域,C++因拥有面向对象特性,非常流行,所以本节以C++语言为例描述渲染引擎架构设计。若不考虑跨平台,Swift也是个不错的选择,语言特性丰富,学习成本低,表达能力强。

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

推荐阅读更多精彩内容