OpenGL ES在iOS系统和Android系统上已经得到大规模的应用。两年多之前做过一些win下面的OpenGL ES的东西,第三方库用了GLEW,然后想弄到iOS系统上去,无奈没有找到之前的源码,也未曾了解过iOS下基于OpenGL ES的一些东西,于是就有了这篇文章。顺带整理一下知识点。
首先简要的梳理几个知识点:
1.CAEAGLLayer
详细的介绍参见苹果官方文档。这个Layer是你在iOS应用上绘制OpenGL内容的Layer。如果想要绘制OpenGL的话,要用这个Layer作为视图方法layerClass的返回值。CAEAGLLayer是为Core Animation封装的,并完全兼容OpenGL ES函数调用。换句话说,要使用OpenGL ES命令就要先创建这个Layer。
这个与View关联的类用于Graph Context绘制的目标。理解为画布就好了。为了减少性能消耗,一般将Layer的opaque属性设置为不透明,即TRUE。
2.EAGLContext
详细介绍参见苹果官方文档。EAGLContext对象用于管理OpenGL ES渲染上下文——例如状态信息、命令、以及需要使用OpenGL ES绘制的资源。想要执行OpenGL ES命令,你就需要创建一个EAGLContext对象。
需要渲染的资源,例如纹理以及渲染缓冲区,通过一个与context关联的EAGLShaderGroup对象管理。在渲染之前,必须绑定一个framebuffer对象到context。
3.帧缓冲区(frame buffer)
图形程序的目标大多就是在屏幕上绘制图形。屏幕是由一个矩形的像素组组成的,每个像素都可以在屏幕上的某个点显示一种颜色的小方块。光栅化阶段之后,数据就不再是像素,而是片段。每个片段具有与像素对应的坐标数据以及颜色和深度值。然后,经过一系列测试和操作,如果顺利通过这些测试和操作,片段值转换为像素。
为了绘制这些像素,需要知道它们的颜色。每个像素按照统一的方式存储时,存储所有像素的空间就叫做缓冲区。不同的缓冲区为每个像素存储的数据量可能不同。但是每个特定的缓冲区内,每个像素存储的数据量是一致的。
例如颜色缓冲区用于保存屏幕上所显示的颜色信息。假设屏幕宽度为w个像素,高度为y个像素,并使用24位完整颜色。那么这个屏幕可显示2^24(16777216)种颜色。因此颜色缓冲区必须为屏幕上的w x h个像素的每一个像素都存储24 / 3 = 8(3个字节)的数据。
颜色缓冲区只是众多缓冲区中的一种(例如还有深度缓冲区,模版缓冲区等)。系统的帧缓冲区是由所有这些缓冲区组成的。
由于各种操作需要在缓冲区之间大量的移动数据,这就是引入帧缓冲对象的原因所在,可以创建自己的帧缓冲区,并使用它们附带的渲染缓冲区来最小化数据复制并优化性能。
执行离屏渲染,更新纹理图像的时候,帧缓冲区对象非常有用。
窗口系统提供的帧缓冲区对象是你的图形服务器的显示系统可用的唯一帧缓冲区,也就是你自己屏幕上可以看到的唯一缓冲区。相较而言,应用程序创建的缓冲区不能在显示器上显示,只支持离屏渲染。
系统窗口提高的帧缓冲区和我们创建的帧缓冲区的另一个不同在于,当窗口创建的时候,系统窗口管理的那些缓冲区就分配他们的缓冲区——颜色、深度、模版等。而应用创建缓冲区对象的时候,需要创建与之相关联的其他缓冲区(颜色、深度等)。
4.颜色缓冲区(render buffer)
颜色缓冲区通常用于绘图。理解为存储绘制结果的缓冲区就好。一般清理屏幕的步骤是设置缓冲区的清除值。例如glClearColor设置清理颜色缓冲区的颜色值。然后再使用glClear传入相关的参数,指定清理的目标缓冲区。
例如清理屏幕操作
glClearColor(1.0, 1.0, 1.0, 1.0); // 设置清理颜色为RGBA都为1
glClear(GL_COLOR_BUFFER_BIT); // 指定清理颜色缓冲区
5.流程
首先使用Xcode创建一个空白的Single View Application。
然后需要创建一个相关的UIView用于绘制OpenGL ES资源。这里我创建了一个类DMGLview,继承UIView,并包含上述提到的CAEAGLLayer以及EAGLContext。
返回Layer对象为CAEAGLLayer
设置不透明
创建EAGLContext对象,并设置当前context为创建的glContext
创建颜色缓冲区以及帧缓冲区,并将颜色缓冲区绑定到帧缓冲区
其中glGenRenderbuffers函数用于分配一个颜色缓冲区编号(实际并未分配内存),glBindRenderBuffer函数用于分配一个颜色缓冲区空间并与glGenRenderbuffers函数所分配的编号关联起来。
- (BOOL)renderbufferStorage:(NSUInteger)target fromDrawable:(id)drawable 函数则用于绑定绘制对象,也就是将之前创建的Layer作为绘制目的地。
glGenFramebuffers函数以及glBindFrramebuffer函数大概意思和上面分配颜色缓冲区是一样的,只是分配的是帧缓冲区。
glFramebufferRenderBuffer是将刚才创建的颜色缓冲区和帧缓冲区对象附加联系起来。这样在颜色缓冲区绘制完成之后,通过如下操作将帧缓冲区结果复制到系统提供的帧缓冲区以显示出来。
在初始化OpenGL view的时候,我们可以将上述步骤都设置好
最后,不要忘记在应用初始化完成(即didFinishLaunchingWithOptions)之后,设置当前窗口的view为OpenGL view
设置视口、清理缓冲区颜色并清理缓冲区
如果没有错误的话,你会看到如下结果
接着,我们如何开启深度测试呢?
默认的OpenGL ES映射到屏幕的坐标为左下角(-1.0,-1.0)右上角(1.0,1.0)。如果没有坐标变换的话,那么我们绘制出的物体坐标以及大小并不能按我们所想。
在OpenGL ES2.0及以后的版本中,都采取可渲染管线编程。好处是可以编程控制GPU处理顶点数据,得到各种牛逼炫酷帅的效果,麻烦的地方就是坐标转换等等都要自己去编程处理。其实想想也对,目前CPU和GPU的处理速度完全不在一个档次上,GPU的瓶颈在于每次等待CPU数据传输的时间。那么,把大量的数据一次性送入GPU,然后在GPU中实现快速运算就成为理所当然的考虑了。这就是采用GPU编程的好处。
OpenGL采用列向量以及列矩阵表示数据。例如:
因此,OpenGL中都是采取左乘。例如将一个物体先旋转再移动(这里假设旋转矩阵为R,移动矩阵为T,物体坐标向量为v),正确的操作顺序就应该是
从3D模型到屏幕的成像所经历的变化是 模型坐标 -> 观察变换 -> 视图变换 -> 剪裁 -> 视口变换。其中,需要我们编程处理的是观察变换以及视图变换这两步。
那么,既然x,y,z就可以表示三维坐标,为什么还要有w分量。w分量是用于处理平行的两条线交于一点的问题。既然平行为什么会相交?因为在透视观察中,会发现一个有趣的现象就是当两条平行的直线延伸到很远的点的时候,看起来就是汇聚于一点的。为了解决这个问题,引入w分量。
另外采用矩阵相乘的形式会引发万向节锁的问题。这个问题我没研究过,就不班门弄斧了。等研究了之后再谈这个问题。
一个模型要转换为屏幕坐标的编程处理过程为:
好了,经过上面的铺垫,开始进入正题——开启iOS的深度测试(这部分很短)。
原本以为iOS中深度测试就是一句glEnable(GL_DEPTH_TEST)就可以搞定的。
结果绘制出来的图像,前后面上下面顺序什么的乱七八糟。总之就是前后面上下面顺序完全错了,完全不像有深度测试的感觉。最后意识到是深度测试没搞定,于是搜了一些资料。处理完成后整理如下:
和颜色缓冲不同的就是使用glRenderbufferStorage()函数进行分配的时候,需要指定缓冲区的宽高。其中第二个参数表示计算精度,值越高越平滑,当然内存也消耗的更大。
接下来关联深度缓冲到帧缓冲区
完了?
我也以为完了,结果渲染出来不是黑屏就是粉红色!!!然后找了一份源码看了一下,发现后面还有一句
加上,正确渲染了。
不明白为什么,希望高手赐教。
最后渲染结果如下(我加了光照):
[1]
参考
- ^本文已获得作者授权,最早发布于 https://www.jianshu.com/p/26c585105244