使用完CocosCreator做了一个跑酷类的简单游戏之后,对图像渲染之类的比较感兴趣,之前学习过CoreAnimation之后,把iOS里面的过程了解了一下,但是并没有深入了解到GPU的内容。OpenGL ES就是查漏补缺吧。
OpenGL基础
CPU & GPU
CPU可以完成每秒十亿次的运算,但是它只能够每秒读写内存两亿次,所以要在每个数据上执行5个或者更多的运算,不然的话处理器的性能会处于次优状态,这种状态叫做“数据饥饿”。同样,GPU上更明显,GPU每秒执行数十亿次但是每秒只能访问内存2亿次。所以GPU总是受限于内存的访问性能上,并且通常需要在每块数据上执行10~30次运算才不会影响整体的图形输出。
OpenGL流程
着色器是独立运行在GPU上的程序。
该流程图展示说了OpenGL运行的过程,在我们定义了定点数据之后,首选是定点着色器处理定点数据,我们需要告诉OpenGL如果处理定点。图元,是告诉OpenGL渲染怎样的数据,这里渲染的是三角形。几何着色器生成辅助的几何线段。光栅化是将几何形状转换成像素,片段着色器决定最后每个像素的颜色(片段着色器包含3D场景的数据,比如光照、阴影、光的颜色等等),混合是混合不同层次的片段,因为在这个阶段要混合alpha值,所以就算片段着色器确定了最终的颜色,但是混合之后也可能不同。
着色器
OpenGL ES在ios上的使用过程封装了着色器的使用过程,不需要我们自己编译和链接着色器。
纹理
2D纹理的坐标是[0,1],将定点的坐标和纹理的坐标进行点对点的绑定之后,片段着色器会对纹理进行插值
我的理解,插值就是将纹理和顶点组成的几何图形之间进行映射,从而判断每个像素的颜色。
MipMap(多级渐远纹理)是用来解决远处同一个物体的纹理问题,因为如果是远处的小的相同物体,如果使用大的贴图会有浪费内存的问题,所以需要传入小的贴图。MipMap像下面这个样子(后一个比前一个小1/4,超过一定的阈值就会使用相应的图片):
CPU & GPU
CPU可以完成每秒十亿次的运算,但是它只能够每秒读写内存两亿次,所以要在每个数据上执行5个或者更多的运算,不然的话处理器的性能会处于次优状态,这种状态叫做“数据饥饿”。同样,GPU上更明显,GPU每秒执行数十亿次但是每秒只能访问内存2亿次。所以GPU总是受限于内存的访问性能上,并且通常需要在每块数据上执行10~30次运算才不会影响整体的图形输出。
缓存(buffer)
GPU和CPU都有自己独占的内存区域,OpenGL ES为两个内存区域间的数据交换定义了缓存(buffer)的概念。缓存是指图形处理器能够控制和管理的连续的RAM。几乎所有程序提供给GPU的数据都应该放入缓存中。为缓存提供数据有如下7个步骤:
- Generate-> glGenBuffers()--请求OpenGL ES生成graphics processor控制的唯一标志。
- Bind-> glBindBuffer()--告诉OpenGL ES使用缓存处理接下来的运算。
- Buffer Data-> glBufferData() or glBufferSubData()--告诉OpenGL ES为当前绑定的buffer分配并初始化足够多的连续内存(通常是从CPU控制的内存复制数据到GPU控制的内存)。
- Eable or Disable -> glEnableVertexAttribArray() or glDisableVertexAttribArray()--告诉OpenGL ES在接下来的渲染中是否使用缓存中的数据。
- Set Pointer -> glVertexAttribPointer()--告诉OpenGL ES buffer里的数据类型和访问buffer数据的任何内存偏移量。
- Draw -> glDrawArrays() or glDrawElements()--告诉OpenGL ES使用当前绑定的和可以使用的缓存来渲染部分或者全部场景。
- Delete -> glDeleteBuffers()--告诉OpenGL ES去删除之前生成的缓存和释放相关联的资源。
帧缓存(frame buffer)
就像GPU提供数据的缓存一样,接收渲染结果的缓冲区叫做帧缓存。可以同时存在很多的帧缓存,并且可以通过OpenGL ES让GPU把渲染结果存储到任意的帧缓存中。有front frame buffer和back frame buffer的概念,front frame buffer控制屏幕上显示的像素颜色和布局,所以一般不会直接渲染到front frame buffer中,这样的话就会让用户看到没有渲染完成的图像。相反程序会把渲染结果保存到包含back frame buffer的其他frame buffer中,当back frame buffer渲染成功之后就会立即与front frame buffer进行交换。
OpenGL ES context
Context 封装了配置OpenGL ES保存在特定平台的数据结构信息。因为OpenGL ES是一个状态机,这意味着在一个程序中配置之后就会一直保留这个值,直到程序修改了这个值。上下文信息可能被保存在CPU所控制的内存中,也可能在GPU所控制的内存中。OpenGL ES Context的内部实现依赖于不同的嵌入式系统和GPU硬件,所以OpenGL ES提供了标准的ANSI C语言函数来与Context交互。OpenGL ES Context会跟踪用于渲染的帧缓存,一会跟踪用于几何数据、颜色等的缓存。
Core Animation与OpenGL ES
iOS操作系统不支持直接访问front frame buffer和back frame buffer,有操作系统来操作,这样的话方便随时使用Core Animation Compositor来控制显示的最终外观。
这个也很好理解,因为core animation是一个单独的进程,不仅仅要生成我们自己应用里面的layer,还要控制app之间的切换,所以这是个系统的工作,没必要暴露出来。
layer保存了所有绘制操作的结果。比如,iOS提供了对象有效的在layer上绘制视频,在layer上绘制淡入淡出的图片。layer content也可以用Core Graphics来绘制,也可以用OpenGL ES直接绘制。不过Core Animation Compositor使用的是OpenGL ES来管理GPU,混合图层和贾环frame buffer的。所以一切通过Core Animation的绘制最终都会涉及OpenGL ES。
GLKit
ios封装了OpenGL ES,我们可以直接使用GLKit来使用OpenGL ES。分别使用GLKView和GLKViewController就可以直接操作OpenGL ES了。
以下是个人理解:GLKBaseEffect封装了片段着色器程序,所以只要向该对象传入纹理或者颜色,就可以直接渲染出来。
OpenGL ES使用
OpenGL ES的使用过程就是操作Buffer的过程(见buffer一节的内容)。下面具体讲一下每个函数:
- glGenBuffers(1,&name):这个方法是用来生成缓存,数量是一个,传入首地址标识。
- glBindBuffer(GL_ ARRAY_BUFFER, name): 声明是一个顶点数组类型的buffer,使用对应name的缓存来处理接下来的运算。
- glBufferData(GL_ARRAY_BUFFER,size,data,usage): 第一个是类型,第二个是需要申请的内存大小,第三个是需要拷贝到GPU内存的数据,第四个是读取频率的标识,用于内存优化
- glEnableVertexAttribArray(1),默认就禁止的,这里需要开启一下。
- glVertexAttribPointer(index,size,GL_FLOAT,GL_FALSE,stribe,pointer)。该函数告诉OpenGL怎样处理传入的顶点。因为在顶点数组中,一般还要保存纹理的UV数据,所以需要指定类型(位置、颜色、纹理之类的)、大小、步幅以及偏移量,这样的话就可以合理使用顶点。
- 下面就是绘制顶点缓存和删除顶点缓存了。
纹理
如果直接使用GLKit提供的方法来初始化纹理的话是非常简单点的,首先根据CGImage对象生成一个GLKTextureInfo对象,然后将该对象的Target和name赋值给baseEffect对象的对应属性就可以了。在准备渲染的时候配置定点的读取规则就可以了。
如果不使用GLKit的话,需要自己手动生成、绑定和配置纹理,与生成缓存类似。
- glGenTexture(1,&id)生成纹理。
- glBindTexture(GL_ TEXTURE_2D, id),绑定纹理。
- glTexImage2D(GL_ TEXTURE,0,GL_RGBA,width,height,0,GL_ RGBA,GL_ UNSIGNED_BYTE, [imageData bytes]),将图片数据保存成纹理
- glTextParameteri(GL_ TEXTURE_2D, GL _ TEXTURE_ MIN_ FILTER,GL_LINEAR)配置纹理的显示。只要是纹理的大小大于或小于图形大小,纹理的布局方式。GL_LINEAR表示的是取颜色的方式是取周围的混合色,这样的纹理是模糊和渐变的。
透明度、混合和多重纹理
当纹理计算出一个完全不透明的fragment color的时候,会直接替换调frame buffer中的color render buffer中对应的pixel color。如果存在不透明的话就需要混合。通过glEnable(GL_BLEND)来开启混合。glBlendFunc(sourceFactor,destinationFactor)来设置混合函数。在每帧渲染的过程中需要替换baseEffect中的Target和name,然后[baseeffect prepareToDraw]会同步状态,这样的话就会绘制两个纹理了。
GLKit提供了多重纹理的渲染,可以直接给baseEffect指定两个纹理,并且指定混合模式,就可以直接混合多种纹理。这也间接可以看出CoreAnimation的层设计也是以此为基础的。
灯光
GPU首先为每个三角形的每个定点执行光线计算,然后把计算的结果插补在顶点之间来修改每个渲染的片元的最终颜色。因此模拟灯光的质量和光滑度要取决于组成每个3D物体的顶点的数量。光线的计算是以三角形为单位的,光线的向量与三角面的法向量的夹角决定了采光量的多少。所以在使用灯光的时候需要在定点中传入法向量的值。
GLKit使用灯光的话就是直接在baseEffect对象里面直接使用light0属性进行设置。设置了这个属性之后,就算没有打开,颜色属性也会失效,而是变成灯光的颜色属性,所以如果还要渲染其他有颜色的物体的时候,需要新建baseEffect实例。
将baseEffect当做片段着色器,并且OpenGL ES Context是一个状态机,所以完全可以将前几个定点绘制成黄色,然后改变baseEffect的颜色填充属性为绿色,再调用绘制,就会将剩下的定点绘制成绿色。
深度测试
深度测试是OpenGL自动完成的,我们只需要告诉OpenGL保存测试深度的大小,一般是16位和24位大小。然后调用深度函数来决定通过的门槛。一般使用默认的即可。在iOS里面的代码设置如下:
glGenRenderbuffers(1, &depthRenderBuffer); // Step 1
glBindRenderbuffer(GL_RENDERBUFFER, // Step 2
depthRenderBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, // Step 3
GL_DEPTH_COMPONENT16,
currentDrawableWidth,
currentDrawableHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, // Step 4
GL_DEPTH_ATTACHMENT,
GL_RENDERBUFFER,
depthRenderBuffer);
坐标系统及变换
在以前使用3DMax(制作3D模型的软件)的时候就会有很多的坐标系可以选择,在做游戏的时候也是可以选择世界坐标或者是本地坐标,原来是OpenGL规定的标准。
对物体的每个点做矩阵变换就是对整个物体的进行移动、缩放、旋转。OpenGL封装好了变换的矩阵,只需要我们传入改变的值就可以生成对应的矩阵。与iOS里面的CoreGraphics或者是CALayer的transform矩阵变换是一样的。列举一下常用的几种变换:
- 围绕一个点旋转:1)平移到所需要的旋转中心。2)施加所需要的旋转。3)使用与第一步相反的平移值平移回来
- 围绕一个点缩放:1)平移到所需要的缩放中心。2)施加想要的缩放。3)使用与第一步相反的平移值平移回来
透视投影是以一个”平截投体“,例下图所示:
其设置的代码如下所示(所传入的属性就是确定平截投体的大小的):
self.baseEffect.transform.projectionMatrix =
GLKMatrix4MakeFrustum(
-1.0 , //left
1.0 , //right
-1.0, //bottom
1.0, //top
1.0, //near
60.0); //far
OpenGL ES剩余内容
剩下的还有动画、读取模型、特效(天空盒、粒子等)这些内容,这部分没有仔细的实现demo,而是大概看了一下,前段时间看了游戏相关的内容,这些部分感觉就是专门为游戏和3D定制的,和应用类的ios开发有些靠不上,所以这里等日后再学习。动画就是在单位时间内(取决于刷新频率)对定点做矩阵变化,骨骼动画就是制定父节点,父节点发生矩阵变化的时候同样也要对子节点做变换,这样的话就会有联动的效果了。因为传递到GPU的都是定点信息,那么模型其实就是顶点信息的文件,读取的过程就是取出顶点、纹理等信息。