1.2.01_ COCOS2D-X渲染之OpenGL

96
希希爸爸
2017.07.04 16:54* 字数 4765

摘自:Cocos2d-x 高级开发教程-第10章OpenGL基础

OpenGL简介(1)

OpenGL是一个基于C语言的三维图形API,基本功能包含绘制几何图形、变换、着色、光照、贴图等

除了基本功能,OpenGL还提供了诸如曲面图元、光栅操作、景深、shader编程等高级功能。

状态机

OpenGL是一个基于状态的绘图模型,我们把这种模型称为状态机。在此模型下,OpenGL时刻维护着一组状态,**这组状态涵盖了一切绘图参数,如即将绘制的多边形、填充颜色、纹理、混合模式和当前的坐标系等。

为了正确地绘制图形,我们需要把OpenGL设置到合适的状态,然后调用绘图指令。例如,为了绘制一个三角形,首先需要设置坐标系、顶点列表以及填充颜色,然后发送绘图指令。

状态机的设计有许多优势。绘图是一件十分复杂的工作。为了绘制图形,我们通常需要设置许多参数(例如坐标变换,填充何种颜色,启用何种颜色混合模式,使用什么格式来描述多边形,纹理的像素格式等),其中许多参数并不频繁改变,因此也没有必要每次都重新设置。

OpenGL把所有的参数作为状态来保存,如果没有设置新的参数,则会一直采用当前的状态来绘图

我们可以把绘图设备人为地分为两个部分:

  • "服务器端",负责具体的绘制渲染
  • "客户端",负责向服务器端发送绘图指令

游戏通常运行在一台设备上,在设备中CPU负责运行游戏的逻辑,并向GPU(硬件显卡或是软件模拟的显卡)发送绘图指令

在这种架构下,CPU和GPU分别充当客户端与服务器端的角色(如下图)。在实际使用中,OpenGL的客户端与服务器端是可以分离的,因此可以轻而易举地实现远程绘图。

CPU|GPU

举例说明,如果需要实现一个远程桌面系统,设备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来绘制

右手三维坐标系

</br>

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)。可编程着色器作为原有渲染管线中一些部分的代替品,不仅可以实现原有的渲染功能,还可以自由实现开发者自定义的渲染效果。利用可编程着色器,开发者可以在渲染过程中自由控制顶点和片段处理采用的算法,以便实现更加炫丽的渲染效果。
    可编程着色器主要包含顶点着色器和片段着色器,其中前者负责对顶点进行几何变换以及光照计算,后者负责处理光栅化得到的像素以及纹理。

</br>

绘图

前面我们简单介绍了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表示即将绘制的图形顶点数量。

</br>

矩阵与变换

作为绘图的一个强大工具,坐标系变换在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 | 右乘一个缩放矩阵

202.Cocos2d-x(3.+)