OpenGL ES2入门01-概念篇
2019-08-27
概述
OpenGL是一个低级编程的API,它可以实现2D和3D图形编程。cocos2d,sparrow,corona,unity 这些框架,你会发现其实它们都是基于OpenGL上创建的。
OpenGL ES
是负责 GPU 工作的,目的是通过 GPU 计算,得到一张图片,这张图 片在内存中其实就是一块 buffer,存储有每个点的颜色信息等。而这张图片最终是要显示到屏幕上,所以还需要具体的窗口系统来操作,OpenGL ES
并没有相关的函数。所以,OpenGL ES
有一个好搭档 EGL。
EGL
全称:embedded Graphic Interface,是 OpenGL ES
和底层 Native 平台 视窗系统之间的接口。所以大概流程是这样的:首先,通过 EGL
获取到手机屏幕 的 handle,获取到手机支持的配置(RGBA8888/RGB565 之类,表示每个像素中包 含的颜色等信息的存储空间是多少位),然后根据这个配置创建一块包含默认 buffer 的 surface(buffer 的大小是根据屏幕分辨率乘以每个像素信息所占大小计 算而得)和用于存放 OpenGL ES
状态集的 context,并将它们 enable 起来。然后, 通过 OpenGL ES
操作 GPU 进行计算,将计算的结果保存在 surface 的 buffer 中。 最后,使用 EGL
将绘制的图片显示到手机屏幕上。
GLSL,全称:OpenGL Shading Language,是 OpenGL ES 中使用到的着色器的 语言,用这个语言可以编写小程序运行在 GPU 上。
在这里需要先提到 CPU 和 GPU 的区别,它们的功能都是用于计算,也都是由很多核组成,区别在于 CPU 的核比较少,但是单个核的计算能力比较强,而 GPU 的核很多,但是每个核的计算能力都不算特别强。目前 GPU 的主要工作是用于生成图片(现在也有通过 GPU 进行高性能运算_并行运算,但是在这里不属于讨论的范围),原因就是图片是由很多像素组成,每个像素都包含有颜色、深度等信息,而为了得到这些信息数据,针对每个像素点的计算,是可以通过统一的算法来完成。GPU 就擅长处理针对这种大规模数据,使用同一个算法进行计算。而这个算法,就是使用 GLSL 写成 Shader,供 GPU 运算使用。
在图形学的视角中,所有的图片都是由三角形构成的。所以通过 OpenGL ES
绘制图片的时候,我们需要通过 OpenGL ES
API 创建用于在 GPU 上运行的 shader, 然后将通过 CPU 获取到的图片顶点信息,传入 GPU 中的 Shader 中。在 Vertex Shader 中通过矩阵变换,将顶点坐标从模型坐标系转换到世界坐标系,再到观察坐标系,到裁剪坐标系,最后投影到屏幕坐标系中,计算出在屏幕上各个顶点的坐标。然后,通过光栅化,以插值的方法得到所有像素点的信息,并在 Fragment shader 中计算出所有像素点的颜色。最后,通过 OpenGL ES
的 API 设定的状态,将得到的像素信息进行 depth/stencil test、blend,得到最终的图片。
重要概念
渲染管线
- 首先,最左边的 API 指的就是 OpenGL ES 的 API,OpenGL ES 其实是一个图形学库,由 109 个 API 组成,只要明白了这 109 个 API 的意义和用途,就掌握了
OpenGL ES 2.0
- 然后,我们通过 API 先设定了顶点的信息,顶点的坐标、索引、颜色等信息,将这些信息传入 VS(顶点着色器)
- 在 VS 中进行运算,得到最终的顶点坐标。再把算出来的顶点坐标进行图元装配,构建成虚拟的线和三角形。再进行光珊化(在光珊化的时候,把顶点连接起来形成直线,或者填充多边形的时候,需要考虑直线和多边形的直线宽度、点的大小、渐变算法以及是否使用支持抗锯齿处理的覆盖算法。最终的每个像素点,都具有各自的颜色和深度值)
- 将光珊化的结果传入 FS(片段着色器),进行最终的颜色计算
- 然后,这所谓最终的结果在被实际存储到绘制 buffer 之前,还需要进行一系列的操作。这些操作可能会修改甚至丢弃这些像素点。这些操作主要为 Alpha测试、深度/模板测试、混合、抖动。
顶点数组
顶点数组是一系列顶点的集合。一个顶点(Vertex)是一个 3D 坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们假定每个顶点只由一个 3D 位置和一些颜色值组成。
为了让 OpenGL 知道我们的坐标和颜色值构成的到底是什么,OpenGL 需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给 OpenGL 。OpenGL 支持三种基本图元:点,线和三角形。
每个图元由一个或者多个顶点组成,每个顶点定义一个点,一条边的一端或者三角形的一个角。每个顶点关联一些数据,这些数据包括顶点坐标,颜色,法向量以及纹理坐标等。所有这些顶点相关的信息就构成顶点数据,这些数据首先被上传到 GL 服务端,然后就可以进行绘制。
顶点着色器
顶点着色器对每个顶点执行一次运算,它可以使用顶点数据来计算该顶点的坐标,颜色,光照,纹理坐标等,在渲染管线中每个顶点都是独立地被执行。
在顶点着色器中最重要的任务是执行顶点坐标变换,应用程序中设置的图元顶点坐标通常是针对本地坐标系的。本地坐标系简化了程序中的坐标计算,但是 GL 并不识别本地坐标系,所以在顶点着色器中要对本地坐标执行模型视图变换,将本地坐标转化为裁剪坐标系的坐标值。
顶点着色器的另一个功能是向后面的片段着色器提供一组易变变量(varying)。易变变量会在图元装配阶段之后被执行插值计算,如果是单重采样,其插值点为片段的中心,如果多重采样,其插值点可能为多个采样片段中的任意一个位置。易变变量可以用来保存插值计算片段的颜色,纹理坐标等信息。
图元装配
在顶点着色器程序输出顶点坐标之后,各个顶点被按照绘制命令中的图元类型参数,以及顶点索引数组被组装成一个个图元。
顶点数组首先通过 GL 命令输入到 GL 渲染管线中,此时顶点坐标位于应用程序的本地坐标系;在经过顶点着色器的计算之后,顶点坐标被转化到裁剪坐标系中,这通常通过向顶点着色器传入一个模型视图变换矩阵,然后在顶点着色器中执行坐标变换。
裁剪坐标系被定义在一个视锥体裁剪的空间里,视锥体是游戏场景的一个可视空间,它由6个裁剪平面构成,分别是:近平面,远平面,左平面,右平面,上平面和下平面。
视锥体在 3D 应用程序中通常表现为一个摄像机,其观察点为裁剪坐标系的原点,方向为穿过远近平面的中点。
处于视锥体以外的图元将被丢弃,如果该图元与视锥体相交则会发生裁剪产生新的图元。值得注意的是透视裁剪是一个比较影响性能的过程,因为每个图元都需要和 6 个面进行相交计算,并产生新的图元。但是一般在x,y方向超出屏幕之外的,则无需产生新的图元,这些顶点能在视口变换的时候被更高效的丢弃。
通过图元装配,所有 3D 的图元已经被转化为屏幕上 2D 的图元。
光栅化
在光栅化阶段,基本图元被转换为供片段着色器使用的片段(Fragment),Fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程。
在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
片段着色器
可编程的片段着色器是实现一些高级特效如纹理贴图,光照,环境光,阴影等功能的基础。片段着色器的主要作用是计算每一个片段最终的颜色值(或者丢弃该片段)。
在片段着色器之前的阶段,渲染管线都只是在和顶点,图元打交道。在 3D 图形程序开发中,贴图是最重要的部分,程序可以通过 GL 命令上传纹理数据至 GL 内存中,这些纹理可以被片段着色器使用。片段着色器可以根据顶点着色器输出的顶点纹理坐标对纹理进行采样,以计算该片段的颜色值。
另外,片段着色器也是执行光照等高级特效的地方,比如可以传给片段着色器一个光源位置和光源颜色,可以根据一定的公式计算出一个新的颜色值,这样就可以实现光照特效。
片段测试
片段着色器输出的颜色值,还要经过几个阶段的片段操作,这些操作可能会修改片段的颜色值,或者丢弃该片段,最终的片段颜色值才会被写入到帧缓冲中。
像素所有权测试用来判断帧缓冲区中该位置的像素是否属于当前 OpenGL ES,例如在窗口系统中该位置可能会被其他应用程序窗口遮挡,此时该像素则不会被显示。
在片段测试之后,片段要么被丢弃,要么每个片段对应的颜色,深度,模板值会被写入帧缓冲区,最终呈现在设备屏幕上。帧缓冲区中的颜色值也可以被读回到客户端应用程序中,这样可以实现绘制到纹理的效果。
至此,OpenGL ES 渲染管道最终将每个像素点的颜色,深度,模板等数据输送到帧缓存中(Framebuffer)。
帧缓存/渲染缓存
帧缓存是接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域。它存储着 OpenGL ES 绘制每个像素点最终的所有信息:颜色,深度和模板值。更通俗点,可以理解成存储屏幕上最终显示的一帧画面的区域。
而渲染缓存则存储呈现在屏幕上的渲染图像,它也被称作颜色缓冲区,因为它本质上是存储要显示的颜色。多个纹理对象或多个渲染缓存对象,可通过连接点(attachment points)连接到帧缓存对象上。
可以同时存在很多帧缓存,并且可以通过 OpenGL ES 让 GPU 把渲染结果存储到任意数量的帧缓存中。但是,只有将内容绘制到视窗体提供的帧缓存中,才能将内容输出到显示设备。视图系统提供的帧缓存通常由两个缓存对象组成,一个前端缓存,一个后端缓存。
前帧缓存决定了屏幕上显示的像素颜色。程序的渲染结果通常保存在后帧缓存在内的其他帧缓存,当渲染后的后帧缓存包含一个完成的图像时,前后帧缓存会立即互换,前帧缓存变成新的后帧缓存,后帧缓存变成新的前帧缓存。
但是前后帧我们无法去操纵,它是由系统控制的。我们只能显式的告诉系统,要展示哪个帧缓存了,然后由系统去完成前后帧的切换。
纹理
纹理是一个用来保存图像的色值的 OpenGL ES 缓存。
现实生活中,纹理最通常的作用是装饰我们的物体模型,它就像是贴纸一样贴在物体表面,使得物体表面拥有图案。
但实际上在 OpenGL 中,纹理的作用不仅限于此,它可以用来存储大量的数据。一个典型的例子就是利用纹理存储画笔笔刷的 mask 信息。
标准化坐标系
纹理坐标系
纹理坐标在 x 和 y 轴上,范围为 0 到 1 之间(我们使用的是 2D 纹理图像)。使用纹理坐标获取纹理颜色叫做采样。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上。