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也是个不错的选择,语言特性丰富,学习成本低,表达能力强。