摘自:Cocos2d-x 高级开发教程-第10章OpenGL基础
OpenGL简介(1)
OpenGL是一个基于C语言的三维图形API,基本功能包含绘制几何图形、变换、着色、光照、贴图等。
除了基本功能,OpenGL还提供了诸如曲面图元、光栅操作、景深、shader编程等高级功能。
状态机
OpenGL是一个基于状态的绘图模型,我们把这种模型称为状态机。在此模型下,OpenGL时刻维护着一组状态,**这组状态涵盖了一切绘图参数,如即将绘制的多边形、填充颜色、纹理、混合模式和当前的坐标系等。
为了正确地绘制图形,我们需要把OpenGL设置到合适的状态,然后调用绘图指令。例如,为了绘制一个三角形,首先需要设置坐标系、顶点列表以及填充颜色,然后发送绘图指令。
状态机的设计有许多优势。绘图是一件十分复杂的工作。为了绘制图形,我们通常需要设置许多参数(例如坐标变换,填充何种颜色,启用何种颜色混合模式,使用什么格式来描述多边形,纹理的像素格式等),其中许多参数并不频繁改变,因此也没有必要每次都重新设置。
OpenGL把所有的参数作为状态来保存,如果没有设置新的参数,则会一直采用当前的状态来绘图。
我们可以把绘图设备人为地分为两个部分:
- "服务器端",负责具体的绘制渲染;
- "客户端",负责向服务器端发送绘图指令。
游戏通常运行在一台设备上,在设备中CPU负责运行游戏的逻辑,并向GPU(硬件显卡或是软件模拟的显卡)发送绘图指令。
在这种架构下,CPU和GPU分别充当客户端与服务器端的角色(如下图)。在实际使用中,OpenGL的客户端与服务器端是可以分离的,因此可以轻而易举地实现远程绘图。
举例说明,如果需要实现一个远程桌面系统,设备A是被控制端,设备B是控制端,我们需要在设备B上呈现设备A中的图形,因此设备A可以通过网络向设备B发送绘图指令,而设备B负责绘制与渲染图形。在这个例子中,设备A就是OpenGL客户端,而设备B是OpenGL服务器端。
在游戏的例子中,绘图指令及数据由CPU发送到GPU,状态机的优势看似并不是十分明显,而在远程绘图的例子中,绘图指令及数据由设备A通过网络发送到设备B,网络的带宽显然是有限的,因此为了提高效率,我们通常把可以在客户端完成的工作分摊给客户端,只把绘图所必需的数据发送到服务器端即可。
事实上,即使是运行在计算机上的游戏,也受益于OpenGL的架构。在计算机上,CPU与GPU通过总线相连,虽然总线的带宽远高于网络连接,但在许多情况下,带宽明显不能满足高速运算的CPU与GPU之间传递数据的需要。因此我们也需要尽力避免在客户端与服务器端传递不必要的数据。
OpenGL提供了许多改变绘图状态的函数。例如,我们可以使用以下函数来开启或关闭绘图特性:
GL_APICALL void GL_APIENTRY glEnable (GLenum cap); //开启一个状态
GL_APICALL void GL_APIENTRY glDisable (GLenum cap); //禁止一个状态
这里的GLenum类型用来表示OpenGL的状态量。后面我们将会看到,全部状态的列表定义在"gl2.h"头文件中。不同的绘图效果需要不同的支持状态,默认情况下,Cocos2d-x只会开启固定的几种状态,必要的时候必须自己主动开启所需状态,使用完毕后主动禁止。例如,为了裁剪渲染区域,就需要设置GL_SCISSOR_TEST状态。
实际上,从"gl2.h"头文件中就可以看出,OpenGL是一个非常接近底层的接口标准,核心部分只包括了约170个函数和约300个常量,与汇编指令的数量相差无几,这也是我们需要用游戏引擎来减轻开发工作量的原因。
坐标系
OpenGL是一个三维图形接口,在程序中使用右手三维坐标系。具体地说,在初始化时,屏幕向右的方向为X方向,屏幕向上的方向为Y方向,由屏幕指向我们的方向为Z方向,下图形象地说明了坐标系的构成。在三维空间中,每一个点都对应一个坐标。为了绘制各种图形,我们需要做的就是利用坐标描绘出图形的形状,然后把形状交给OpenGL来绘制。
OpenGL简介(2)
OpenGL负责把三维空间中的对象通过投影、光栅化转换为二维图像,然后呈现到屏幕上。在Cocos2d-x中,我们只需要呈现二维的图像,因此Z坐标只用作控制游戏元素的前后顺序,通常不做讨论。
为了呈现精灵,引擎会根据精灵的位置创建矩形,在OpenGL中设置矩形的顶点以及纹理,把图形绘制并呈现到屏幕上。下图简单地描述了三维图形如何呈现到屏幕上。
在不对OpenGL做任何设置的时候,初始的坐标系称作世界坐标系,我们当然可以在世界坐标系中完成所有绘图。
然而如果真的这么做,为了把一个物体绘制到不同的位置,我们就不得不去修改物体的所有顶点坐标。在游戏开发中,物体的顶点少则三个,多则上千个,对于每一次绘图都刻意地计算一次坐标是一项十分繁重的任务,甚至当我们需要通过相对坐标计算绝对坐标的时候,这项任务几乎难以完成。
为了解决这个问题,OpenGL提供了坐标系变换的功能。除了世界坐标系以外,OpenGL还维护了一个绘图坐标系。绘图坐标系在初始化时与世界坐标系重叠,它也可以通过调用变换函数(例如平移、旋转和缩放)来随时改变。当我们绘制图形的时候,OpenGL会把图形绘制在当前的绘图坐标系中。
以《捕鱼达人》的游戏场景的控制栏为例,控制栏是放置在屏幕下方的一片区域,其中包含了金钱数量、倒计时、道具和炮台等元素,每一个元素的坐标相对于控制栏的左下角来定位。其中,炮台的中心位于控制栏的中心处,可以沿着此点旋转方向。在绘制控制栏时,引擎首先在屏幕左下角的位置确定绘图坐标系,绘制控制栏背景,如图b所示,然后在屏幕正下方的位置确定绘图坐标系,在此处绘制炮台,如图c所示。
渲染流水线
当我们把绘制的图形传递给OpenGL后,OpenGL还要进行许多操作才能完成3D空间到屏幕的投影。通常,渲染流水线过程(如图所示)有如下几步:显示列表、求值器、顶点装配、像素操作、纹理装配、光栅化和片断操作等。
OpenGL ES 1.0版本中采用的是固定渲染管线。在固定渲染管线模型中,每一个步骤的操作都是固定的,开发者只能使用OpenGL所提供的渲染模型,无法进行更改。
OpenGL从2.0版本开始引入了可编程着色器(shader)。可编程着色器作为原有渲染管线中一些部分的代替品,不仅可以实现原有的渲染功能,还可以自由实现开发者自定义的渲染效果。利用可编程着色器,开发者可以在渲染过程中自由控制顶点和片段处理采用的算法,以便实现更加炫丽的渲染效果。
可编程着色器主要包含顶点着色器和片段着色器,其中前者负责对顶点进行几何变换以及光照计算,后者负责处理光栅化得到的像素以及纹理。
绘图
前面我们简单介绍了OpenGL的工作原理以及基本概念,在这一节中,我们将介绍OpenGL的几个绘图函数。随着OpenGL的发展,其提供的绘图函数也变得多种多样。
对于同一个效果来说,常常有多种不同的实现方法,因此想要在此对OpenGL的绘图函数进行全方位的介绍是不可能的,这里我们只简单介绍Cocos2d-x中常用的绘图函数。
下面我们从一个简单的例子开始介绍,在这个例子中,我们需要向Cocos2d-x Hello World项目中添加一些代码。打开Hello World项目,并在"HelloWorldScene.h"中的HelloWorld类中重载void draw()方法: virtual void draw();
void HelloWorld::draw()
{
//顶点数据
static GLfloat vertex[] =
{ //顶点坐标:x,y,z
0.0f, 0.0f, 0.0f, //左下
200.0f, 0.0f, 0.0f, //右下
0.0f, 200.0f, 0.0f, //左上
200.0f, 200.0f, 0.0f, //右上
};
static GLfloat coord[] =
{ //纹理坐标:s,t
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
static GLfloat color[] =
{ //颜色:红色、蓝色、绿色、不透明度
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
};
//初始化纹理
static CCTexture2D* texture2d = NULL;
if(!texture2d)
{
texture2d = CCTextureCache::sharedTextureCache()->addImage("HelloWorld.png");
coord[2] = coord[6] = texture2d->getMaxS();
coord[1] = coord[3] = texture2d->getMaxT();
}
//设置着色器
ccGLEnableVertexAttribs(kCCVertexAttribFlag_PosColorTex);
texture2d->getShaderProgram()->use();
texture2d->getShaderProgram()->setUniformForModelViewProjectionMatrix();
//绑定纹理
glBindTexture(GL_TEXTURE_2D, texture2d->getName());
//设置顶点数组
glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, 0, vertex);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, 0, coord);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_FLOAT, GL_FALSE, 0, color);
//绘图
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
运行Hello World项目,我们就可以看到在游戏画面的左下角出现了一个200×200大小的Cocos2d-x标志。
回顾一下刚才的代码,通过注释我们可以大概了解到draw方法中每一条语句的含义。draw大致上可以分为3个部分--数据部分、初始化纹理和绘图,它绘制了一个带纹理的矩形。
事实上,我们也可以通过绘制一个"三角形带(triangle stripe)"来绘制。因为矩形实际上是两个包含公共斜边的直角三角形,所以绘制这样两个三角形,将它们的斜边相连,就可以拼成一个矩形。
三角形带是计算机图形学中的一个重要概念,若要了解更多相关知识,可以参考任何一本计算机图形学方面的图书。
-
第一部分是数据部分,在这一部分中我们声明了3个静态数组,它们分别是vertex、coord和color,对应了三角形带中共4个顶点的顶点坐标、纹理坐标和顶点颜色。每个数组均按照左下、右下、左上、右上的顺序来存储。
- vertex:共4个顶点,每个顶点包含x、y和z三个分量,因此顶点坐标数组共有12个值。在本例中,矩形位于屏幕左下角,大小为200×200。
- coord:包含s和t(横坐标和纵坐标)两个分量,因此共有8个值,每个分量的取值范围是0到1,需要根据纹理的属性确定取值。
- color:包含r、g、b和a(红色、绿色、蓝色和不透明度RGBA)4个分量,因此共有16个值,每个分量的取值范围是0~1。把颜色值设为纯白(1, 1, 1, 1),则会显示纹理原来的颜色。
第二部分是初始化纹理。利用CCTextureCache类可以方便地从文件中载入一个纹理,获取纹理尺寸,以及获取纹理在OpenGL中的编号。
在纹理没有被初始化时,我们首先使用CCTextureCache::addImage方法载入一个图片,把返回的CCTexture2D对象保存下来,并使用纹理的属性设置4个顶点的纹理坐标。对于单个纹理的图片,只需要按照上面代码中的方法设置纹理坐标即可。-
最后一部分是绘制图片。绘制图片的步骤可以简述为:绑定纹理、设置顶点数组和绘图。
- 绑定纹理是指把一个曾经载入的纹理当做当前纹理,从此绘制出来的多边形都使用此纹理。
- 设置顶点数组是指为OpenGL指定第一步准备好的顶点坐标数组、纹理坐标数组以及顶点颜色数组。
- 绘图则是最终通知OpenGL如何利用刚才提供的信息进行绘图,并实际把图形绘制出来。
在这个过程中,我们可以看到最重要的一个函数为glDrawArrays(GLenum mode, GLint first, GLsizei count),其中mode指定将要绘制何种图形,first表示前面数组中起始顶点的下标,count表示即将绘制的图形顶点数量。
矩阵与变换
作为绘图的一个强大工具,坐标系变换在OpenGL开发中被广泛采用。为了理解坐标系变换,首先需要了解一些坐标系变换所需的数学知识,这些知识也是计算机图形学的数学基础。
OpenGL对顶点进行的处理实际上可以归纳为接受顶点数据、进行投影、得到变换后的顶点数据这3个步骤。当我们设置好OpenGL的坐标系,并传入顶点数据后,OpenGL就会通过一系列计算把顶点映射到世界坐标系之中,再把世界坐标系中的点通过投影变换为可视平面上的点。这一系列变换的本质是通过对顶点坐标进行线性运算,得到处理后的顶点坐标。
在计算机中,坐标变换是通过矩阵乘法实现的,用向量表示坐标,矩阵表示变换形式,则变换后的顶点坐标可以用向量与矩阵的乘法来表示。使用矩阵乘法的优点在于,计算机(包括移动设备)的图形硬件通常对矩阵乘法进行了大量优化,从而大大提高了运算效率。
点、向量与矩阵
在计算机中,通常不直接使用与点维度数量一样的向量来表示一个点,因为这样就无法利用矩阵乘法来对点进行平移等操作了。因此,在计算机图形学中,通常采用齐次坐标来表示一个顶点。具体地说,齐次坐标系中每一个点的维度比顶点维度多1,多出的一个维度值为1。对于任何三维中的顶点(x, y, z),它在齐次坐标系中的向量为[x, y, z, 1],例如,空间中的(1.2, 5, 10)对应的向量为[1.2, 5, 10, 1]。
变换利用矩阵表示。常见的变换包含平移变换、旋转变换和缩放变换等,它们分别对应了平移矩阵、旋转矩阵和缩放矩阵等。下面以平移矩阵为例,展示如何使用矩阵乘法实现坐标变换。平移矩阵为
其中(tx,ty,tz)为平移的方向向量。若我们希望把点(1.2, 5, 10)平移(6, 5, 4)距离,则计算矩阵的乘法如下:
可以看到,我们得到了平移后的点(7.2, 10, 14)。上面是对一个点进行一次变换的情况,如果希望对点进行多次变换,则应该依次构造每个变换对应的矩阵,并利用矩阵乘法把所有矩阵与顶点向量相乘。例如,对点P依次进行缩放、平移、缩放和旋转操作,则分别构造它们对应的变换S1、T、S2、R,按照如下公式计算变换后的点P':
OpenGL维护了一个当前绘图矩阵,用于表示当前的绘图坐标系。这个矩阵被初始化为单位矩阵,此时绘图坐标系与世界坐标系相同,当我们不断地在绘图矩阵后乘上新的矩阵时,会相应地改变绘图坐标系。
在上面的例子中,R × S2 × T × S1即为绘图矩阵,它表示了一个绘图坐标系。在此点上绘制的P点坐标经过映射后,可以得到它在世界坐标系中对应的坐标P'。
OpenGL为我们提供了一系列创建变换矩阵的函数(如表所示),因此,在实际开发中,我们并不需要手动构造变换矩阵。这些函数的作用是创建一个变换矩阵,并在当前绘图矩阵的后方乘上这个矩阵。现在对刚才的例子稍作修改,我们不再希望只对点P进行一系列变换,而是希望对一个完整的图形进行变换。以下代码绘制一个任意的图形,并将此图形首先放大2.5倍,然后平移(1, 2, 3)距离,最后缩小0.8倍:
//OpenGL ES 1.0
glScalef(0.8f, 0.8f, 0.8f); //乘上缩放矩阵
glTranslatef(1.0f, 2.0f, 3.0f); //乘上平移矩阵
glScalef(2.5f, 2.5f, 2.5f); //乘上缩放矩阵
DrawObject(); //绘制任意图形
常见的OpenGL ES 1.0变换函数
OpenGL ES 1.0函数 | 替代函数 | 描述
-- |
glPushMatrix | kmGLPushMatrix | 把矩阵压栈
glPopMatrix | kmGLPopMatrix | 从矩阵栈中弹出
glMatrixMode | kmGLMatrixMode | 设置当前矩阵模式
glLoadIdentity | kmGLLoadIdentity | 把当前矩阵置为单位矩阵
glLoadMatrix | kmGLLoadMatrix | 设置当前矩阵的值
glMultMatrix | kmGLMultMatrix | 右乘一个矩阵
glTranslatef | kmGLTranslatef | 右乘一个平移矩阵
glRotatef | kmGLRotatef | 右乘一个旋转矩阵
glScalef | kmGLScalef | 右乘一个缩放矩阵