文章目录
  1. 1. 自定义绘图
  2. 2. 遮罩层
  3. 3. 数据交流
  4. 4. 可编程管线
    1. 4.1. 可编程着色器
    2. 4.2. CCGLProgress
    3. 4.3. 变量传递
  5. 5. CCGrid3D
  6. 6. 再议效率

自定义绘图

引擎在CCNode类中为我们预留了自定义绘图的接口,具体如下所示:

void CCNode::draw(){
}

cocos2dx提供了一些简单的快捷绘制接口实现最简单的功能,这些接口由CCDrawingPrimitives.h和对应的cpp文件提供。

遮罩层

对于稍微复杂的绘图效果,就需要调用底层的OpenGL接口,游戏中经常出现的滚动数字表盘用来显示倒计时,可以使用OpenGL提供的遮罩效果来快速实现。

遮罩效果又称为剪刀效果,允许一切的渲染结果只在屏幕的一个指定区域显示:开启遮罩效果后,一切的绘制提交都是正常渲染的,但最终只有屏幕上的指定区域会被绘制。形象地说,我们将当前屏幕截图成一张固定的画布盖在屏幕上,只挖空指定的区域使之能活动,而屏幕上的其他位置尽管如常更新,但都被掩盖住了。 于是,我们可以在表盘上顺序排列所有的数字,不该显示的部分用遮罩效果盖住,滚动的表盘效果可以借助遮罩得到快速的实现。

要实现遮罩效果,需要重载visit方法

//启动遮罩效果
glEnable(GL_SCISSOR_TEST);

//设置遮罩效果
glScissor(....);

//关闭遮罩效果
glDisable(GL_SCISSOR_TEST);

数据交流

底层的数据交流必须介绍两个类:CCImage和CCTexture2D,这是引擎提供的描述纹理图片的类,也是我们和显卡进行数据交换时主要涉及的数据结构。

CCImage在”CCImage.h”中定义,表示一张加载到内存的纹理图片。在其内部的实现中,纹理以每个像素的颜色值保存在内存之中。CCImage通常作为文件和显卡间数据交换的一个工具,因此主要提供了两个方面的功能:一方面是文件的加载与保存,另一方面是内存缓冲区的读写。

我们可以使用CCImage轻松地读写图片文件。目前,CCImage支持PNG、JPEG和TIFF三种主流的图片格式。下面列举与文件读写相关的方法:

bool initWithImageFile(const char* strPath, EImageFormat imageType = kFmtPng);
bool initWithImageFileThreadSafe(const char* fullpath, EImageFormat imageType = kFmtPng);
bool saveToFile(const char* pszFilePath, bool bIsToRGB = true);

CCImage也提供了读写内存的接口。getData和getDataLen这两个方法提供了获取当前纹理的缓冲区的功能,而initWithImageData方法提供了使用像素数据初始化图片的功能。相关的方法定义如下:

unsigned char* getData();
int getDataLen();
bool initWithImageData(void* pData,int nDataLen,EImageFormat eFmt = kFmtUnKnown,int nWidth = 0,int nHeight = 0,int nBitsPerComponent = 8);
目前仅支持从内存中加载RGBA8888格式的图片。

CCTexture2D,它描述了一张纹理,知道如何将自己绘制到屏幕上。通过该类还可以设置纹理过滤、抗锯齿等参数。该类还提供了一个接口,将字符串创建成纹理。

该类所包含的纹理大小必须是2的幂次,因此纹理的大小不一定就等于图片的大小;另外,有别于CCImage,这是一张存在于显存中的纹理,实际上并不一定存在于内存中。

我们使用OpenGL的一个底层函数glReadPixels实现截图:

void glReadPixels (GLint x, GLint y,GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);

这个函数将当前屏幕上的像素读取到一个内存块pixels中,且pixels指针指向的内存必须足够大。为此,我们设计一个函数saveScreenToCCImage来实现截图功能,相关代码如下:

unsigned char screenBuffer[1024 * 1024 * 8];
CCImage* saveScreenToCCImage(bool upsidedown = true)
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSizeInPixels();
    int w = winSize.width;
    int h = winSize.height;
    int myDataLength = w * h * 4;

    GLubyte* buffer = screenBuffer;
    glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buffer);

    CCImage* image = new CCImage();
    if(upsidedown) {
        GLubyte* buffer2 = (GLubyte*) malloc(myDataLength);
        for(int y = 0; y <h; y++) {
            for(int x = 0; x <w * 4; x++) {
                buffer2[(h - 1 - y) * w * 4 + x] = buffer[y * 4 * w + x];
            }
        }
        bool ok = image->initWithImageData(buffer2, myDataLength,
        CCImage::kFmtRawData, w, h);
        free(buffer2);
    }else {
        bool ok = image->initWithImageData(buffer, myDataLength,CCImage::kFmtRawData, w, h);
    }
    return image;
}

我们使用glReadPixels方法将当前绘图区的像素都读取到了一个内存缓冲区内,然后用这个缓冲区来初始化CCImage并返回。注意,我们设置了一个参数upsidedown,当这个参数为true时,我们将所有像素倒序排列了一次。这是因为OpenGL的绘制是从上到下的,如果直接使用读取的数据,再次绘制时将上下倒置

我们在游戏菜单层中添加相关按钮和响应操作就完成了截屏功能,相关代码如下:

void GameMenuLayer::saveScreen(CCObject* sender)
{
    CCImage* image = saveScreenToCCImage();
    image->saveToFile("screen.png");
    image->release();
}

引擎还提供了另一个很有趣的方法让我们完成截图功能。在Cocos2d-x中,我们实现了一个渲染纹理类CCRenderTexture,其作用是将绘图设备从屏幕转移到一张纹理上,从而使得一段连续的绘图被保存到纹理中。这在OpenGL的底层中并不罕见,有趣的地方就在于,我们可以使用这个渲染纹理类配合主动调用的绘图实现截图效果。下面的函数saveScreenTo- RenderTexture同样实现了截图功能:

CCRenderTexture* saveScreenToRenderTexture()
{
    CCSize winSize = CCDirector::sharedDirector()->getWinSize();
    CCRenderTexture* render = CCRenderTexture::create(winSize.height, winSize.width);

    render->begin();
    CCDirector::sharedDirector()->drawScene();
    render->end();

    return render;
}

渲染纹理提供了两个导出纹理的接口,分别可以导出纹理为CGImage和文件,它们的定义如下:

CCImage* newCCImage();
bool saveToFile(const char *name, tCCImageFormat format);

CCRenderTexture继承自CCNode,,其导出纹理的过程实际上也是利用glReadPixels函数来获取像素信息。因此,导出纹理这一步的效率和我们自己编写的saveScreenToCCImage函数是一致的。然而如果采用重新绘制的方式来导出纹理则与此不同,一次屏幕的过程较为费时,尤其在布局比较复杂的场景上。此时最好采用CCRenderTexture.

可编程管线

我们可以通过着色器定义每一个顶点或像素的着色方式,产生更丰富的效果。着色器实际上就是一小段执行渲染效果的程序,由图形处理单元执行。之所以说是”一小段”,是因为图形渲染的执行周期非常短,不允许过于臃肿的程序,因此通常都比较简短。

可编程着色器

有两种:

  1. 顶点着色器(vertex shader)。对每个顶点调用一次,完成顶点变换(投影变换和视图模型变换)、法线变换与规格化、纹理坐标生成、纹理坐标变换、光照、颜色材质应用等操作,并最终确定渲染区域。在Cocos2d-x的世界中,精灵和层等都是矩形,它们的一次渲染会调用4次顶点着色器。
  2. 段着色器(fragment shader,又称片段3着色器)。这个着色器会在每个像素被渲染的时候调用,也就是说,如果我们在屏幕上显示一张320×480的图片,那么像素着色器就会被调用153 600次。所幸,在显卡中通常存在不止一个图形处理单元,渲染的过程是并行化的,其渲染效率会比用串行的CPU执行高得多。

    两者不能单独使用,二者协同工作。

    CCGLProgress

    引擎提供了CCGLProgram类来处理着色器相关操作,对当前绘图程序进行了封装,其中使用频率最高的应该是获取着色器程序的接口:

    const GLuint getProgram();
    该接口返回了当前着色器程序的标识符。这里返回的是一个无符号整型的标识符,而不是一个指针或结构引用,这是OpenGL接口的一个风格。对象(纹理、着色器程序或其他非标准类型)都是使用整型标识符来表示的。

CCGLProgram提供了两个函数导入着色器程序,支持直接从内存的字符串流载入或是从文件中读取。这两个函数的第一个参数均指定了顶点着色器,后一个参数则指定了像素着色器:

bool initWithVertexShaderByteArray(const GLchar* vShaderByteArray,const GLchar* fShaderByteArray);
bool initWithVertexShaderFilename(const char* vShaderFilename,const char* fShaderFilename);

变量传递

仅仅加载肯定是不够的,我们还需要给着色器传递运行时必要的输入数据。在着色器中存在两种输入数据,分别被标识为attribute和uniform。

attribute变量是应用程序直接传递给顶点着色器的变量,在段着色器中不能访问。它描述的是每个顶点的属性,如位置、法线等,被限制为
向量或标量这样的简单结构。必须为每个顶点指定对应的值,这类似于C中的函数参数。

uniform变量是全局性的,可以同时在顶点着色器和段着色器中访问。在整个渲染流水线中,每个uniform变量都是唯一的,不存在每个像素
或顶点需要单独定义的问题,这一点是和C的全局变量类似的。uniform变量的可定义类型会更丰富一些,还可以包括纹理矩阵和纹
理,甚至可以通过uniform block自定义复杂的数据类型。

虽然都被称为”变量”,但这仅仅是对于应用程序而言的。在着色器程序中,不管是顶点着色器还是段着色器,这些变量都是只读的,不允许在渲染过程中改变。

以上两种变量的传递都要经过获取位置和设置两步。

CCGrid3D

引擎封装了一个特殊的动作类CCActionGrid3D,可以模拟一些简单的3D特效,在一些情况下可以代替OpenGL。恰好引擎利用CCActionGrid3D提供了一个类似于我们实现的水纹效果的波浪效果动作,下面我们就利用Cocos2d-x自带的动作来实现水纹效果。

这个特效动作类的使用非常简单,先看如何用其代替我们之前实现的效果。在开始场景的初始化中加入下面的两行代码:

CCGrid3DAction *grid = CCWaves3D::create(50, 40, ccg(10, 10), 10);
this->runAction(grid);

与自定义着色器相比,CCActionGrid3D局限于表现一些使画面变形的效果,其本质是将目标节点所在区域划分为网格,对每一个小网格进行坐标变换从而形成画面的特殊扭曲。正因为此,它无法改变光照与颜色的渲染方式。

再议效率

引擎提供的封装效果在效率上是一定会有损失的。以CCActionGrid3D为例,全屏使用其波浪效果,帧数基本下降到了个位数,究其原因,是产生了太多的绘制,如果我们划定网格大小为10×10,320×480的屏幕上就存在着32×48=1536个网格,也就是一帧要额外进行1536次绘制,产生的消耗代价是较大的。再例如CCDrawingPrimitive中的简单绘制效果,它能够绘制出简单的几何图形,但一条线段就会耗费一次绘制,代价不小。

另一个值得明确的是,OpenGL ES 2.0提供的可编程管线在效率上必然是弱于OpenGL ES 1.0的硬件渲染的,即便是实现同样的效果。由于在可编程管线的架构上,这些效果实际上是内置的程序,比起硬件的渲染效果,效率必然会有损失。另一方面,我们在引入自定义着色器特效时,也容易在着色器本身以及内存和显卡间数据交换两方面形成效率损失。

任何新技术都是一把双刃剑,我们在使用任何底层绘图的时候都务必注重效率。

文章目录
  1. 1. 自定义绘图
  2. 2. 遮罩层
  3. 3. 数据交流
  4. 4. 可编程管线
    1. 4.1. 可编程着色器
    2. 4.2. CCGLProgress
    3. 4.3. 变量传递
  5. 5. CCGrid3D
  6. 6. 再议效率