文章目录
  1. 1. OpenGL基础
    1. 1.1. OpenGL简介
      1. 1.1.1. 状态机
        1. 1.1.1.1. 状态机的优势
        2. 1.1.1.2. 开启关闭绘图特性
      2. 1.1.2. 坐标系
      3. 1.1.3. 渲染流水线
    2. 1.2. 绘图
      1. 1.2.1. 绘图过程
    3. 1.3. 矩阵和变换
  2. 2. Cocos2dx绘图原理
    1. 2.1. 精灵绘制
      1. 2.1.1. 流程
    2. 2.2. 渲染树的绘制
      1. 2.2.1. 绘制流程
    3. 2.3. 坐标变换
  3. 3. TexturePacker与优化
    1. 3.1. 绘图瓶颈
    2. 3.2. 碎图压缩与精灵框帧
    3. 3.3. 批量渲染
    4. 3.4. 色彩深度优化

游戏引擎是对底层绘图接口的包装,cocos2dx也一样,是对OpenGL的包装。

OpenGL基础

OpenGL全称Open Graphics Library,是一个开放的、跨平台的高性能图形接口。OpenGL ES则是OpenGL在移动设备上的衍生版本。

OpenGL简介

OpenGL是一个基于C语言的三维图形API,基本功能包括绘制几何图形、变换、着色、光照、贴图等。除了基本功能外,OpenGL还提供了诸如曲面图元、光栅操作、景深、shader编程等高级功能。

状态机

OpenGL是一个基于状态的绘图模型,我们把这种模型称作状态机。在此模型下,OpenGL时刻维护着一组状态,这组状态涵盖了一切绘图参数。

状态机的优势
  1. 绘制图形我们会需要设置许多参数,其中许多参数并不频繁改变,没有必要每次都重复设置,OpenGL把所有的参数作为状态保存,如果没有设置新的参数,则会一直采用当前的状态来绘图
  2. 可以把绘图设备人为地分为两个部分:”服务器端”,负责具体的绘制渲染;“客户端”,负责向服务器端发送绘图指令。两者分离,可以轻而易举实现远程绘图。 但是通常情况下带宽不能满足CPU和GPU之间传递数据的需要,因此我们需要尽力避免在客户端与服务器端传递不必要的数据。

    开启关闭绘图特性

    GL_APICALL void GL_APIENTRY glEnable(GLenum cap); //开启一个状态
    GL_APICALL void GL_APIENTRY glDisable(GLenum cap); //GU关闭一个状态

    坐标系

    三维图形接口,使用右手三维坐标系。在初始化时,屏幕向右的方向为X方向,屏幕向上的方向为Y方向,由屏幕指向我们的方向为Z方向。

OpenGL负责把三维空间中的对象通过投影、光栅化转化为二维图像,然后呈现到屏幕上。

渲染流水线

OpenGL需要许多操作才能完成3D空间屏幕的投影。通常,渲染流水线过程有如下几步:显示列表、求值器、顶点装配、像素操作、纹理装配、光栅化和片段操作等。

OpenGL ES 1.0版本采用固定渲染管线,每个步骤都是固定不可修改的。

OpenGL从2.0开始引入可编程着色器(shader),可以自定义渲染效果。可编程着色器主要包含顶点着色器和片段着色器。其中前者负责对顶点进行几何变换以及光照计算,后者负责处理光栅化得到的像素以及纹理。

绘图

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);

}

绘图过程

  1. 数据部分
  2. 初始化纹理
  3. 绘制图片

矩阵和变换

OpenGL对顶点进行的处理实际上可以归纳为接受顶点数据、进行投影、得到变换后的顶点数据这三个步骤。在计算机中,坐标的变换是通过矩阵乘法实现的。

OpenGL ES 为我们提供了一系列创建变换矩阵的函数,在1.0中,有如下变换函数:

glTranslate      平移
glRotate         旋转
glScale          缩放

而在2.0中,放弃了渲染流水线,取而代之的是自定义的各种着色器,一般需要开发者来维护。引擎引入第三方库Kazmath,分装成了一些替代函数,如:

kmGLTranslatef   平移
kmGLRotatef      旋转
。。。。

Cocos2dx绘图原理

精灵绘制

void CCSprite::draw(void)
{
//1. 初始准备

CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
CCAssert(!m_pobBatchNode, "If CCSprite is being rendered by CCSpriteBatchNode,
CCSprite#draw SHOULD NOT be called");
CC_NODE_DRAW_SETUP();

//2. 颜色混合函数

ccGLBlendFunc(m_sBlendFunc.src, m_sBlendFunc.dst);

//3. 绑定纹理

if (m_pobTexture != NULL)
{
     ccGLBindTexture2D(m_pobTexture->getName());
}
 else
{
 ccGLBindTexture2D(0);
}

//4. 绘图
ccGLEnableVertexAttribs(kCCVertexAttribFlag_PosColorTex);

#define kQuadSize sizeof(m_sQuad.bl)
long offset = (long)&m_sQuad;

//顶点坐标
int diff = offsetof(ccV3F_C4B_T2F, vertices);
glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE,
kQuadSize, (void*) (offset + diff));

//纹理坐标
diff = offsetof(ccV3F_C4B_T2F, texCoords);
glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE,
kQuadSize, (void*)(offset + diff));

//顶点颜色
diff = offsetof(ccV3F_C4B_T2F, colors);
glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE,
kQuadSize, (void*)(offset + diff));

//绘制图形
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

 CHECK_GL_ERROR_DEBUG();


//5. 调试相关的处理

#if CC_SPRITE_DEBUG_DRAW == 1
//调试模式1:绘制边框
CCPoint vertices[4]={
    ccp(m_sQuad.tl.vertices.x,m_sQuad.tl.vertices.y),
    ccp(m_sQuad.bl.vertices.x,m_sQuad.bl.vertices.y),
    ccp(m_sQuad.br.vertices.x,m_sQuad.br.vertices.y),
    ccp(m_sQuad.tr.vertices.x,m_sQuad.tr.vertices.y),
};
ccDrawPoly(vertices, 4, true);
#elif CC_SPRITE_DEBUG_DRAW == 2
//调试模式2:绘制纹理边缘
CCSize s = this->getTextureRect().size;
CCPoint offsetPix = this->getOffsetPosition();
CCPoint vertices[4] = {
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};
ccDrawPoly(vertices, 4, true);
#endif

CC_INCREMENT_GL_DRAWS(1);

CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, "CCSprite - draw");
}

流程

  1. 设置OpenGL状态,如开启贴图
  2. 设置颜色混合模式,与贴图的渲染方式有关
  3. 绑定纹理
  4. 绘图:分别设置顶点坐标、纹理坐标以及顶点颜色,最终绘制几何体。
  5. 和调试相关的一些操作。

渲染树的绘制

每一帧都是从根节点开始绘制,然后不断绘制子节点,直到绘制完成整个渲染树。

void CCNode::visit()
{
    //1. 先行处理
    if (!m_bIsVisible)
    {
        return;
    }
    kmGLPushMatrix(); //矩阵压栈

    //处理Grid特效
    if (m_pGrid && m_pGrid->isActive())
    {
        m_pGrid->beforeDraw();
    }

    //2. 应用变换
    this->transform();

    //3. 递归绘图
    CCNode* pNode = NULL;
    unsigned int i = 0;

    if(m_pChildren && m_pChildren->count() > 0)
    {
        //存在子节点
        sortAllChildren();
        //绘制zOrder < 0的子节点
        ccArray *arrayData = m_pChildren->data;
        for( ; i < arrayData->num; i++ )
        {
            pNode = (CCNode*) arrayData->arr[i];

        if ( pNode && pNode->m_nZOrder < 0 )
        {
            pNode->visit();
        }
        else
        {
            break;
        }
    }
    //绘制自身
    this->draw();

    //绘制剩余的子节点
    for( ; i < arrayData->num; i++ )
    {
        pNode = (CCNode*) arrayData->arr[i];
        if (pNode)
        {
            pNode->visit();
        }
    }
    }
    else
    {
        //没有子节点:直接绘制自身
        this->draw();
    }

    //4. 恢复工作
    m_nOrderOfArrival = 0;

    if (m_pGrid && m_pGrid->isActive())
    {
        m_pGrid->afterDraw(this);
    }

    kmGLPopMatrix(); //矩阵出栈
}

绘制流程

  1. 先行的处理,例如当此节点被设置为不可见时,则直接返回不进行绘制等。在这一步中,重要的环节是保存当前的绘图矩阵,也就是注释中的”矩阵压栈”操作。绘图矩阵保存好之后,就可以根据需要对矩阵进行任意的操作了,直到操作结束后再通过”矩阵出栈”来恢复保存的矩阵。由于所有对绘图矩阵的操作都在恢复矩阵之前进行,因此我们的改动不会影响到以后的绘制。
  2. ,visit方法调用了transform方法进行一系列变换,以便把自己以及子节点绘制到正确的位置上
  3. 绘图。visit方法中进行了一个判断:如果节点不包含子节点,则直接绘制自身;如果节点包含子节点,则需要对子节点进行遍历,具体的方式为首先对子节点按照ZOrder由小到大排序,首先对于ZOrder小于0的子节点,调用其visit方法递归绘制,然后绘制自身,最后继续按次序把ZOrder大于0的子节点递归绘制出来。经过这一轮递归,以自己为根节点的整个渲染树包括其子树都绘制完了。
  4. 绘制后的一些恢复工作。这一部分中重要的内容就是把之前压入栈中的矩阵弹出来,把当前矩阵恢复成压栈前的样子

坐标变换

在绘制渲染树中,最关键的步骤之一就是进行坐标系的变换。没有坐标系的变换,则无法在正确的位置绘制出纹理。同时,坐标系的变换在其他的场合(例如碰撞检测中)也起着十分重要的作用。因此,我们将介绍Cocos2d-x中的坐标变换功能。

void CCNode::transform()
{
    kmMat4 transfrom4x4;

    //获取相对于父节点的变换矩阵transform4x4
    CCAffineTransform tmpAffine = this->nodeToParentTransform();
    CGAffineToGL(&tmpAffine, transfrom4x4.mat);

    //设置z坐标
    transfrom4x4.mat[14] = m_fVertexZ;

    kmGLMultMatrix( &transfrom4x4 ); //当前矩阵与transform4x4相乘


    //处理摄像机与Grid特效
    if ( m_pCamera != NULL && !(m_pGrid != NULL && m_pGrid->isActive()) )
    {
        bool translate = (m_tAnchorPointInPoints.x != 0.0f ||m_tAnchorPointInPoints.y != 0.0f);

    if( translate )
        kmGLTranslatef(RENDER_IN_SUBPIXEL(m_tAnchorPointInPoints.x),
    RENDER_IN_SUBPIXEL(m_tAnchorPointInPoints.y), 0 );

    m_pCamera->locate();

    if( translate )
        kmGLTranslatef(RENDER_IN_SUBPIXEL(-m_tAnchorPointInPoints.x),
    RENDER_IN_SUBPIXEL(-m_tAnchorPointInPoints.y), 0 );
}

TexturePacker与优化

当游戏规模变大,屏幕上精灵数量激增,精灵执行的动作越来越复杂,游戏帧率会随之下降,可能会出现明显的延迟现象。

绘图瓶颈

  1. 纹理过小:OpenGL在显存中保存的纹理的长宽像素数一定是2的幂,对于大小不足的纹理,则在其余部分填充空白,这无疑是对显存极大的浪费;另一方面,同一个纹理可以容纳多个精灵,把内容相近的精灵拼合到一起是一个很好的选择。
  2. 纹理切换次数过多:GPU开销大,可以批量绘制一些内容相近的精灵,就可以考虑利用这个特点来减少纹理切换的次数
  3. 纹理过大:显存是有限的,如果在游戏中不加节制地使用很大的纹理,则必然会导致显存紧张,因此要尽可能减少纹理的尺寸以及色深。

下面就将介绍几种优化方式

碎图压缩与精灵框帧

碎片压缩:将许多零碎的小图片合并到一张大图里(可以使用工具TexturePacker),导出一个Plist文件。

使用过程中,先将Plist加载到引擎中

CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("all.plist");

CCSprite提供了接口让我们从精灵帧完成初始化:
canno = CCSprite::createWithSpriteFrameName(cannonPath);

批量渲染

在同一纹理下的绘制可以使用批量提交,CCSpriteBatchNode可以一次批量提交所有子节点的绘图请求,以减少提交次数,提高绘图性能。

色彩深度优化

在低端机型中个,内存、显存资源有限,我们需要降低色彩深度来控制游戏尺寸。

默认情况下,我们导出的纹理图片是RGBA8888格式,它的含义是每个像素的红、蓝、绿、不透明度4个值分别占用8比特(一个字节),因此一个像素总共需要使用4个字节。如果降低纹理的品质,可以采用RGBA4444来保存图片,对于不透明的图片,可以选择无Alpha通道的颜色格式,例如RGB565。

文章目录
  1. 1. OpenGL基础
    1. 1.1. OpenGL简介
      1. 1.1.1. 状态机
        1. 1.1.1.1. 状态机的优势
        2. 1.1.1.2. 开启关闭绘图特性
      2. 1.1.2. 坐标系
      3. 1.1.3. 渲染流水线
    2. 1.2. 绘图
      1. 1.2.1. 绘图过程
    3. 1.3. 矩阵和变换
  2. 2. Cocos2dx绘图原理
    1. 2.1. 精灵绘制
      1. 2.1.1. 流程
    2. 2.2. 渲染树的绘制
      1. 2.2.1. 绘制流程
    3. 2.3. 坐标变换
  3. 3. TexturePacker与优化
    1. 3.1. 绘图瓶颈
    2. 3.2. 碎图压缩与精灵框帧
    3. 3.3. 批量渲染
    4. 3.4. 色彩深度优化