OpenGL ES 开发随想:基础篇

发表于2015-11-03
评论0 4.1k浏览
  闲暇时间开发了一款iOS系统下的基于OpenGL ES 2.0的渲染引擎,这里就简单把OpenGL的一些东西讲一下吧,自己也可以顺便复习一下这块的知识。同时写给那些对OpenGL感兴趣的同学。

说明一下,这篇文章并不是OpenGL教程,只是为了让大家更好地理解OpenGL。由于开发过程中用的是OpenGL ES 2.0,因此文章中关于OpenGL的东西全部都是基于OpenGL ES 2.0来讲的。

那么OpenGL是啥?它是个图形库。那这个库是做什么的?用来驱动GPU画图,专业一点讲就是渲染图像,嗯,就是你现在屏幕上看到的所有东西都是它画出来的。

具体来说OpenGL是怎么进行渲染的呢?这个渲染的流程类似于一个工厂里面的流水线(如下图),你在一端把各种材料送进去,然后另外一端就把东西生产出来了,具体来说就是一张图像。当然,我们肯定是可以控制生成的图像是什么样子的,下图中的Vertex和Fragment部分就是我们进行控制的地方,就是可编程的部分。
上面的流程图被称之为可编程管线(programmable pipeline)。可编程意味着这个流程的可控程度非常高,基本上我们需要的效果都可以通过编程来实现。不过缺点就是基本上所有事情都需要我们来做。举个例子,这里我们传了一个红色的正方形进去,想着在屏幕上出现一个漂亮的红色正方形,不过运行后却发现是一片黑屏。为什么呢?因为我们需要进行非常具体的编程来告诉OpenGL这是个正方形,放在屏幕的某某位置,红色的。这里我们用到的编程语言叫做GLSL(OpenGL Shading Language),具体的情况后面有机会再讲吧。

接下来讲一下怎么用OpenGL,下面的示意图是一个标准的OpenGL程序运行时的构成,红色部分是必要模块,灰色是可选模块。下图基本上是上面的管线图的运行时的表现。下面简单介绍一下这几个模块。

FrameBuffer

FrameBuffer简单讲就是一个OpenGL输出图像的缓存,一个程序可以设置多个FrameBuffer来存储图像,但最终需要有一个FrameBuffer把当前的渲染结果输出到显示屏上。有一些程序是使用两个FrameBuffer进行交替渲染和显示,可以提高效率。一个FrameBuffer包含了三种图像缓存,分别为颜色,深度和模板,如下图所示,其中深度和模板这两种缓存是可选的,可以在用到时才打开。

ColorAttachment,顾名思义就是保存颜色的地方,屏幕上显示就是这个地方的内容,不过这里的图像并不都是输出到屏幕的,有时也会输出到纹理缓存中,作为下一次渲染时的素材使用。

DepthAttachment用于保存图像的每个点的深度信息,点越黑说明越接近屏幕,主要用于判断哪个图形在哪个图形前面,而且重叠图形相交的地方也可以正确表现出来。

StencilAttachment保存了图形的轮廓信息,一般是用来当遮罩裁剪图像用的。

Program

Program在这里表示的是一个在GPU运行的程序,之前提到的可编程部分有两个,Vertex和Fragment。一个Program就是把这两部分的编程代码(GLSL)进行编译打包,然后传给GPU运行。

VertexShader(顶点着色器),它的主要任务就是告诉OpenGL图形的顶点位置信息,当然更多时候这里不仅仅处理顶点信息,类似于灯光计算,纹理坐标,法线计算等都会在这里进行。下面是一段简单的VertexShader代码演示。
        
举个例子说明一下上面这段代码,现在我们要渲染一个红色的正方体,需要传入正方体的6个顶点信息和一个用于转换的矩阵。对于每一个顶点,这段代码都会执行一次。每次执行时VertexPosition都会传入一个顶点坐标,与MVPMatrix相乘得出一个新的坐标信息输出到gl_Position,用于渲染管线的下一阶段处理。

FragmentShader(片元着色器),主要是计算画布上某一个点的颜色信息。这里的代码也是会多次执行的,执行的次数是根据图像内容在画布上占用的点数来算的,比如说把刚才那个正方体缩的很小,只占10个点,那么这里就只会执行10次。但如果把这个正方体放大到整个屏幕,占640X960个点,那这里就会执行614400次。正常情况下这里的负荷会比VertexShader要重,因此有时候我们会把一些计算转移到VertexShader中,可以提高渲染效率。下面是一段简单的FragmentShader代码演示:
      
这里只是把一个固定的红色颜色信息写入到gl_FragColor中,实际中这里的颜色计算可能会很复杂,比如计算纹理映射,光线的反射效果,阴影效果等。

关于点的概念,这里的点不是指屏幕像素,而是指创建FrameBuffer缓存时指定的长宽单位。

Render Resources

这里的资源都是GPU中的缓存,运行时数据从CPU拷贝到GPU的。

VertexBuffer用于表示顶点信息。这里顶点的信息不仅仅顶点的位置,还可以是该顶点对应的纹理坐标,法线方向等。如下图所示,类似于一个结构数组,结构里面定义了位置,纹理坐标,法线等变量。每次渲染时,OpenGL都会从这个buffer中获取顶点信息,传入VertexShader进行计算。

上面的VertexBuffer总共包含了4个顶点信息,每个顶点包含了一个三维坐标,一个二维的纹理映射坐标和一个三围法线方向。这些信息可以用来计算模型的纹理以及光照效果,根据不同的需求这里的结构是可以进行变更的,比如说你不需要纹理,那这里的UV部分可以删掉,剩下点坐标和法线,可以渲染出一个单一颜色的带光照效果的模型。

IndiceBuffer是表示顶点索引信息,这里是个简单的整形数组(unsigned short或unsigned char),里面记录了VertexBuffer的顶点索引,配合VertexBuffer使用,主要用于减少数据传输,提高绘制效率。

Texture表示纹理,就是一张图片,主要是在FragmentShader中使用的,作为一项资源用于计算某个点的颜色。Texture本身可以创建非常多个,只要不把GPU的内存占满就行了,不过一个shader中同时加载的纹理个数是有限的,像iOS设备一般是8个,看起来很少,不过一般是够用的。

关于资源调用与状态机

第一次看OpenGL代码时,我们会发现有很多的glGenXXX, glBindXXX,glEnable(), glDisable()等函数,很多人都会感到很困惑,为什么不直接set,而是需要各种glGenXXX,glBindXXX,然后再进行各种操作。那是因为OpenGL运行时是一个状态机的机制。


怎么理解这个状态机呢,这里可以把OpenGL想象成一个炒菜的机器人,它的功能就是炒菜(渲染)。你可以对它进行编程,炒出各种菜。不过就像大多数炒菜机器人一样,它不会自己准备材料,所以你要帮它准备好材料。机器人有几个槽位提供给我们用于放置各种食材,同时提供食材的几种处理功能,但每种功能只提供一个槽位,同时你有很多个盒子,盒子可以装东西,然后放到槽位上。这里机器人的槽位只接收盒子,请不要把盘子之类的奇怪东西放到上面。现在我们以番茄炒蛋为例说一下这个机器人的操作。

  1. 取一个放番茄的盒子
  2. 放到机器人的清洗槽位
  3. 让机器人对番茄进行清洗
  4. 取一个放鸡蛋的盒子
  5. 放到机器人的打蛋槽位
  6. 让机器人把蛋打好
  7. 告诉机器人番茄在清洗槽位
  8. 告诉机器人鸡蛋在打蛋槽位
  9. 选择番茄炒蛋程序
  10. 执行番茄炒蛋操作
  11. 完成
就像你看到的OpenGL调用一样,整个过程非常繁琐。每炒一个菜我们都需要细心地把材料准备好,告诉它哪个槽位放了什么东西,做什么操作。上面的流程基本上只要有一步出错,最终的结果就是失败的。

这里,glGenXXX类似于获取一个盒子,glBindXXX类似于把这个盒子放到机器人的一个槽位里面。glBufferData(),glTexImage2D()等函数类似于洗番茄,打蛋等操作。glUseProgram就是选择菜单,最后glDrawXXX函数就是执行操作了。

这里只是一个简单的比喻帮助大家理解状态机是怎么一回事,实际情况会比这个复杂,但大体的流程就是这样的。

上面讲到只是OpenGL的一些非常基本的概念,帮助大家对OpenGL有个基本的印象,实际上还是省略了不少东西的。如果你想进一步了解的话可以下载附件的资料,这些资料是我平时收集的,算是比较不错的学习资料。

最后宣传一下我的渲染引擎~目前这个引擎实现了一些最基础的功能:

  •  obj文件加载
  • 三种类型模型的渲染(实体,带alpha贴图,半透明)。
  • 模型贴图(支持png,jpeg,pvr三种格式,其中png和jpeg支持mipmap)
  • 三种光照效果(方向光,点光源,聚光灯)
  • 法线贴图和阴影(仅限于方向光)
  • 资源预处理以及打包
  • 运行时按照渲染配置动态加载shader(非UberShader)
  • 模型数据以及纹理数据等资源的简单管理

如果你是刚接触OpenGL的话,可以下载前面一点版本,最新的版本已经把基本的渲染架构搭起来了,很多东西已经模块化,OpenGL调用比较分散,不太好学习。对了,这个引擎基本上是由OC编写的,你还需要一台装有xcode的mac才能运行这个工程。这个工程仅仅是个人学习OpenGL用的,因此所有代码都是开源的。

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引