cocos2d-x v3.3 TextureAtlas

在游戏制作过程中,很可能会遇到很多相同的、较小尺寸的图片,例如tile-base类的游戏。

<img title = '1357469064_9850.png' src='http://cdn.cocimg.com/bbs/attachment/Fid_41/41_410339_8fc89ebb709060d.png' > 

如果这些图片分别存储(例如一张图片一个文件)、单独渲染,将会托慢游戏整体的渲染效率。所以通常将一些相同的、较小尺寸并且经常使用的图片拼接组成为一张图片(集)交给OpenGL一次渲染,使用的时候再通过x、y、w、h(坐标x和y以及宽和高)从图片集中将使用的图片“抠”出来。

TextureAtlas便是cocos2d-x用来管理这一张图片集(纹理集)的类。首先来看看继承关系:

<img title = '1436361860144.png' src='http://cdn.cocimg.com/bbs/attachment/Fid_41/41_410339_8298b6ee7808806.png' > 

1、成员变量:

protected:
    GLushort*           _indices;    // 索引序列,将会存入IBO。
    GLuint              _VAOname;    // 如果引擎配置为支持VAO,该变量将会存储OpenGL分配的VAO ID(用于操作VAO)。
    GLuint              _buffersVBO;    // 存储OpenGL分配的VBO ID。0:用于存储vertex的VBO ID;1:用于存储indices的VBO ID(IBO)。
    bool                _dirty;    // 指示VBO是否需要更新。
    ssize_t _totalQuads;    // 需要被渲染的quad的总数。
    ssize_t _capacity;    // 当前的纹理集可以存储多少个quad。
    Texture2D* _texture;    // 纹理集的纹理,就是前面说到的多张小图片拼接在一起的那张图片集。
    V3F_C4B_T2F_Quad* _quads;    // cocos2d-x所组装的,传递给OpenGL渲染一个方形图片的基本信息单位,其中包含了方形图片的四个顶点的信息(左上、左下、右上、右下),每个顶点的信息中包含该顶点的3D坐标(x, y, z)、颜色(r, g, b, a)以及UV坐标(u, v)。
    
#if CC_ENABLE_CACHE_TEXTURE_DATA    // 这个宏好像定义的是Android游戏由前台切换到后台时是否清除texture缓存,暂不研究,之后代码暂不分析。
    EventListenerCustom* _rendererRecreatedListener;
#

关键点总结:

♂ VBO:显卡存储空间里的一块缓存区,用于记录顶点的信息,包括3D坐标、uv坐标、法线以及颜色。这个Buffer有它的名字(VBO的ID),OpenGL在GPU的某处记录着这个ID和对应的显存地址(或者地址偏移,类似内存)。
IBO:索引缓冲区。与VBO类似也是显卡存储空间里的一块缓存区,只不过里面存储的是顶点的索引信息。
VAO:是一个对象,绑定一个或多个VBO以及一个IBO(一个图形有多个顶点,一个VBO记录其中一个顶点的信息,多个VBO组合记录图形所有顶点的信息),用于记录渲染对象的完整信息。

♂ UV坐标是u,v纹理贴图坐标的简称(它和空间模型的X, Y, Z轴是类似的),它定义了图片上每个点的位置信息。这些点与3D模型是相互联系的, 以决定表面纹理贴图的位置。UV就是将图像上每一个点精确对应到模型物体的表面,在点与点之间的间隙位置由软件进行图像光滑插值处理,这就是所谓的UV贴图。

♂ IBO的存在是为了避免向OpenGL传递重复的顶点信息。例如渲染一个立方体,立方体有6个面,8个顶点。渲染逐个面进行(6次渲染),每次渲染需要向OpenGL传递4个顶点的信息(每个面4个顶点)。这样渲染整个立方体,每个顶点的信息实际重复传递了两次(一共传递了三次),这样效率很低。实际上顶点信息的全集就是8个顶点的信息,向OpenGL传递这8个顶点的信息,之后再传递一个索引序列(IBO中存储的值),告诉OpenGL渲染第一个面的时候使用(0, 1, 2)号顶点;渲染第二个面的时候使用(1, 2, 3)号顶点……,这样就避免了重复顶点信息的传递。

2、成员方法:

(1) TextureAtlas();
virtual ~TextureAtlas();

实现源码:

TextureAtlas::TextureAtlas()
    :_indices(nullptr)    // 索引序列初始为空。之后在initWithTexture()中会分配空间并初始化值。
    ,_dirty(false)    // VBO初始不需要更新(当然,还没向OpenGL申请VBO呢)。
    ,_texture(nullptr)    // 该纹理集所对应的纹理初始为空。之后在initWithFile()中通过Image类加载图片文件创建该纹理。
    ,_quads(nullptr)    // 需要被渲染的quads数组初始为空。之后在initWithTexture()中会分配空间。
#if CC_ENABLE_CACHE_TEXTURE_DATA
    ,_rendererRecreatedListener(nullptr)
#endif
{}

TextureAtlas::~TextureAtlas()
{
    CCLOGINFO("deallocing TextureAtlas: %p", this);

    // 释放quads数组以及索引序列所占用的内存空间。
    CC_SAFE_FREE(_quads);
    CC_SAFE_FREE(_indices);

    glDeleteBuffers(2, _buffersVBO);    // 释放向OpenGL申请的VBO(VBO和IBO)。

    if (Configuration::getInstance()->supportsShareableVAO())    // 如果引擎配置为支持VAO。
    {
        glDeleteVertexArrays(1, &_VAOname);    // 释放向OpenGL申请的VAO。
        GL::bindVAO(0);    // 解绑VAO。就好象free后的指针最好将其赋值为NULL一样。
    }

/* VBO不用像VAO那样显式解绑?
* glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
* glBindBuffer(GL_ARRAY_BUFFER, 0);
*/

    CC_SAFE_RELEASE(_texture);    // 释放纹理所占用的内存空间(这里准确的说是释放对该纹理的占有权)。

#if CC_ENABLE_CACHE_TEXTURE_DATA
    Director::getInstance()->getEventDispatcher()->removeEventListener(_rendererRecreatedListener);
#endif
}

(2) static TextureAtlas* create(const std::string& file , ssize_t capacity);
static TextureAtlas* createWithTexture(Texture2D *texture, ssize_t capacity);

两个函数均会初始化纹理,并且初始化该纹理集可以存储多少个quad。区别在于第一个函数比第二个函数多了通过给定的图片文件创建纹理的步骤,第二个函数通过参数直接给定了已经创建好的纹理。

file:指定的用于创建纹理集的图片(集)。

capacity:该纹理集可以存储多少个quad。

texture: 该纹理集所对应的纹理。

实现源码:

TextureAtlas * TextureAtlas::create(const std::string& file, ssize_t capacity)
{
    TextureAtlas * textureAtlas = new (std::nothrow) TextureAtlas();    // 创建TextureAtlas对象。
    if(textureAtlas && textureAtlas->initWithFile(file, capacity))    // 如果TextureAtlas对象创建成功并且之后的初始化均成功。
    {
        textureAtlas->autorelease();    // 在一帧结束时自动释放对象。
        return textureAtlas;
    }
    CC_SAFE_DELETE(textureAtlas);    // 如果创建对象或是初始化失败,则释放对象所占内存空间。
    return nullptr;
}

bool TextureAtlas::initWithFile(const std::string& file, ssize_t capacity)
{
    Texture2D *texture = Director::getInstance()->getTextureCache()->addImage(file);        // 通过Image类加载指定的文件创建纹理。

    if (texture)
    {   
        return initWithTexture(texture, capacity);    // 初始化成员变量,申请VAO、VBO并初始化。
    }
    else
    {
        CCLOG("cocos2d: Could not open file: %s", file.c_str());
        return false;
    }
}

bool TextureAtlas::initWithTexture(Texture2D *texture, ssize_t capacity)
{
    CCASSERT(capacity>=0, "Capacity must be >= 0");    // TextureAtlas存储quad的数量非负。

//    CCASSERT(texture != nullptr, "texture should not be null");
    _capacity = capacity;    // 初始化TextureAtlas可存储多少个quad。
    _totalQuads = 0;    // 当前TextureAtlas存储的quad个数为0。

    // retained in property
    this->_texture = texture;    // TextureAtlas所对应的纹理指向之前通过Image类创建的纹理。
    CC_SAFE_RETAIN(_texture);        // 在TextureAtlas::create()中该纹理被autorelease(),而该纹理可能在之后仍会使用,所以在这里retain。

    /* 对TextureAtlas初始化后,TextureAtlas::_quads和TextureAtlas::_indices均会被分配内存空间。
     * 这里限定不允许重复初始化,否则会导致内存泄漏。
     */
    CCASSERT(_quads == nullptr && _indices == nullptr, "");

    // 依据指定的TextureAtlas::_capacity为TextureAtlas::_quads分配内存空间。
    _quads = (V3F_C4B_T2F_Quad*)malloc( _capacity * sizeof(V3F_C4B_T2F_Quad) );
    /* 依据指定的TextureAtlas::_capacity为TextureAtlas::_indices分配内存空间。
  • 这里乘6的原因是之后对quad进行渲染的时候是通过GL_TRIANGLES的方式进行的(见TextureAtlas::drawNumberOfQuads()中,line 644),
    * quad代表一个方形图片,如果通过画三角形来渲染,需要画两个三角形拼接在一起,每个三角形三个顶点,两个三角形有六个顶点,所以这里就需要6个索引。
    */
    _indices = (GLushort *)malloc( _capacity * 6 * sizeof(GLushort) );

      /* 如果TextureAtlas::_quads和TextureAtlas::_indices任意一个分配空间失败,
       * 并且不是由于TextureAtlas::_capacity为0造成的
       * (TextureAtlas不需要存储quad,还分配哪门子空间)。
       */
      if( ! ( _quads && _indices) && _capacity > 0)
      {
          //CCLOG("cocos2d: TextureAtlas: not enough memory");
          CC_SAFE_FREE(_quads);    // 释放TextureAtlas::_quads所占内存空间。
          CC_SAFE_FREE(_indices);    // 释放TextureAtlas::_indices所占内存空间。
    
          // release texture, should set it to null, because the destruction will
          // release it too. see cocos2d-x issue #484
          CC_SAFE_RELEASE_NULL(_texture);    // 释放对纹理的占有权。
          return false;
      }
    
      memset( _quads, 0, _capacity * sizeof(V3F_C4B_T2F_Quad) );
      memset( _indices, 0, _capacity * 6 * sizeof(GLushort) );
    

    #if CC_ENABLE_CACHE_TEXTURE_DATA
    /** listen the event that renderer was recreated on Android/WP8 */
    _rendererRecreatedListener = EventListenerCustom::create(EVENT_RENDERER_RECREATED, CC_CALLBACK_1(TextureAtlas::listenRendererRecreated, this));
    Director::getInstance()->getEventDispatcher()->addEventListenerWithFixedPriority(_rendererRecreatedListener, -1);
    #endif

      this->setupIndices();    // 初始化索引序列。
    
      if (Configuration::getInstance()->supportsShareableVAO())    // 如果引擎配置为支持VAO。
      {
          setupVBOandVAO();    // 申请VAO、VBO以及IBO并初始化。
      }
      else
      {
          setupVBO();    // 申请VBO以及IBO并初始化。
      }
    
      _dirty = true;    // 指示VBO需要更新(这里的更新难道不是多余的?VBO刚刚被初始化)。
    
      return true;
    

    }

    void TextureAtlas::setupIndices()
    {
    if (_capacity == 0) // 不存储quad时无需初始化索引序列。
    return;

// 这里为什么将索引序列初始化为如下值,不明白,哪位大神能给我讲讲?
for( int i=0; i < _capacity; i++)
{
_indices* = i4+0;
_indices
= i4+1;
_indices
= i*4+2;

        // inverted index. issue #179
        _indices* = i*4+3;
        _indices* = i*4+2;
        _indices* = i*4+1;        
    }
}

void TextureAtlas::setupVBOandVAO()
{
    glGenVertexArrays(1, &_VAOname);    // 向OpenGL申请VAO。
    GL::bindVAO(_VAOname);    // 绑定VAO(接下来对VAO的操作均是对该VAO进行操作)。

#define kQuadSize sizeof(_quads.bl)

    // 向OpenGL申请VBO。实际上是申请一个VBO和一个IBO,由于申请方式相同,引擎将它们都做为VBO管理了。
    glGenBuffers(2, &_buffersVBO);

    glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO);    // 绑定VBO(接下来对VBO的操作均是对该VBO进行操作)。
    // 将quads数组通过VBO传递给OpenGL。
    glBufferData(GL_ARRAY_BUFFER, sizeof(_quads) * _capacity, _quads, GL_DYNAMIC_DRAW);

// 当引擎配置为只支持VBO时,以下三段出现在TextureAtlas::drawNumberOfQuads()中;而为何引擎配置为支持VAO时,以下三段出现在这里?*****
// 初始化3D坐标信息。
glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_POSITION);
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof( V3F_C4B_T2F, vertices));

    // 初始化颜色信息。
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_COLOR);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (GLvoid*) offsetof( V3F_C4B_T2F, colors));

    // 初始化UV坐标信息。
    glEnableVertexAttribArray(GLProgram::VERTEX_ATTRIB_TEX_COORD);
    glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof( V3F_C4B_T2F, texCoords));

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO);    // 绑定IBO(接下来对IBO的操作均是对该IBO进行操作)。
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices) * _capacity * 6, _indices, GL_STATIC_DRAW);    // 将索引序列通过IBO传递给OpenGL。

    // 解绑VAO、VBO以及IBO。
    GL::bindVAO(0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    CHECK_GL_ERROR_DEBUG();    // 检查OpenGL是否返回了错误。
}

void TextureAtlas::setupVBO()
{
    // 向OpenGL申请VBO。实际上是申请一个VBO和一个IBO,由于申请方式相同,引擎将它们都做为VBO管理了。
    glGenBuffers(2, &_buffersVBO);

    mapBuffers();    // 初始化VBO以及IBO。
}

void TextureAtlas::mapBuffers()
{
    // Avoid changing the element buffer for whatever VAO might be bound.

// 这里对VAO解绑的意义不太明白。
GL::bindVAO(0);

    glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO);    // 绑定VBO(接下来对VBO的操作均是对该VBO进行操作)。
    glBufferData(GL_ARRAY_BUFFER, sizeof(_quads) * _capacity, _quads, GL_DYNAMIC_DRAW);    // 将quads数组通过VBO传递给OpenGL。
    glBindBuffer(GL_ARRAY_BUFFER, 0);    // 解绑VBO。

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO);    // 绑定IBO(接下来对IBO的操作均是对该IBO进行操作)。
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(_indices) * _capacity * 6, _indices, GL_STATIC_DRAW);    // 将索引序列通过IBO传递给OpenGL。
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);    // 解绑IBO。

    CHECK_GL_ERROR_DEBUG();    // 检查OpenGL是否返回了错误。
}

// 与TextureAtlas::create()相比不用通过文件创建纹理,直接调用TextureAtlas::initWithTexture()。
TextureAtlas * TextureAtlas::createWithTexture(Texture2D *texture, ssize_t capacity)
{
    TextureAtlas * textureAtlas = new (std::nothrow) TextureAtlas();
    if (textureAtlas && textureAtlas->initWithTexture(texture, capacity))
    {
        textureAtlas->autorelease();
        return textureAtlas;
    }
    CC_SAFE_DELETE(textureAtlas);
    return nullptr;
}

关键点总结:

♂ Image类支持的文件格式如下:
class Image
{
enum class Format
{
//! JPEG
JPG,
//! PNG
PNG,
//! TIFF
TIFF,
//! WebP
WEBP,
//! PVR
PVR,
//! ETC
ETC,
//! S3TC
S3TC,
//! ATITC
ATITC,
//! TGA
TGA,
//! Raw Data
RAW_DATA,
//! Unknown format
UNKOWN
};
}

♂ 关于GL_TRIANGLES方式下为何需要6个索引,见https://www.opengl.org/wiki/Primitive中"Triangle primitives"章节。关于为何不使用GL_QUADS方式进行渲染,在"Quads"章节的"Warning"中有提及,好象是从OpenGL 3.1开始这种方式被废弃了。

(3) void updateQuad(V3F_C4B_T2F_Quad* quad, ssize_t index);

将指定的quad更新到quad数组中指定的位置。

quad:指定的待更新的quad。

index:指定更新的位置。

实现源码:

void TextureAtlas::updateQuad(V3F_C4B_T2F_Quad *quad, ssize_t index)
{
    // 指定的位置不能超过quad数组的存储能力。
    CCASSERT( index >= 0 && index < _capacity, "updateQuadWithTexture: Invalid index");

// 在index > TextureAtlas::_totalQuads的情况下,这里为什么要放大TextureAtlas::_totalQuads不太明白。
_totalQuads = MAX( index+1, _totalQuads);

    _quads = *quad;    // 将quad更新到quad数组指定的位置。

    _dirty = true;    // VBO需要更新。
}

(4) void insertQuad(V3F_C4B_T2F_Quad* quad, ssize_t index);

将指定的quad插入到quad数组中指定的位置。


void insertQuads(V3F_C4B_T2F_Quad* quads, ssize_t index, ssize_t amount);

将指定的多个连续存放的quads(quad数组)插入到quad数组中指定的位置。


void insertQuadFromIndex(ssize_t fromIndex, ssize_t newIndex);

将fromIndex处的quad移动到newIndex处,受影响的quads向前或向后移动。

quad:指定的待插入的quad。

index:指定插入的位置。

amount:指定quads数组中有多少个quad需要插入到TextureAtlas::_quads中。

fromIndex:待移动quad的起始位置。

newIndex:待移动quad的目的位置。

实现源码:

void TextureAtlas::insertQuad(V3F_C4B_T2F_Quad *quad, ssize_t index)
{
    CCASSERT( index>=0 && index<_capacity, "insertQuadWithTexture: Invalid index");        // 指定的位置不能超过quad数组的存储能力。

/* 这两句精简为这一句是不是更优雅些?
* CCASSERT( ++_totalQuads <= _capacity, “invalid totalQuads”);
*/
_totalQuads++; // 新插入一个quad,TextureAtlas存储的quad总数就要加1。
CCASSERT( _totalQuads <= _capacity, “invalid totalQuads”); // 存储的quad总数不能超过存储能力。

    /* 当index >= (TextureAtlas::_totalQuads-1)时代表新增的quad是追加在已有quads的后面的,此时计算出的remaining <= 0。
     * 这里的“追加”有两层含义,一是紧挨着最后一个quad放在其之后(index == (TextureAtlas::_totalQuads-1));
     * 另一个是放在最后一个quad后面,中间隔着若干个quads(index > (TextureAtlas::_totalQuads-1))。
     * index < (TextureAtlas::_totalQuads-1)时代表新增的quad是插入到已有quads中的,此时计算出的remaining > 0。
     */
    auto remaining = (_totalQuads-1) - index;

    // last object doesn't need to be moved
    if( remaining > 0)    // 如果新增的quad是插入到已有quads中的。
    {
        memmove( &_quads,&_quads, sizeof(_quads) * remaining );    // 则将index之后的quads均向后移动一个位置。
    }

/* 这里有个问题。
* 如果重复向一个远大于TextureAtlas::_totalQuads同时小于TextureAtlas::_capacity的index处通过调用该函数插入quad,
* 那么在TextureAtlas::_totalQuads自增至其大于index之前,所有的quad不都是覆盖的效果?
* 比如TextureAtlas::_totalQuads是1;TextureAtlas::_capacity是10。
* 调用该函数,index是7。那么在TextureAtlas::_totalQuads自增到9之前所有插入的quad都放到了
* TextureAtlas::_quads的位置,之后的覆盖之前的,导致之前的quads均丢失了。
*/
_quads = *quad;

    _dirty = true;    // VBO需要更新。
}

// 此函数的实现与TextureAtlas::insertQuad()类似,只不过多了个amount的概念。同样存在TextureAtlas::insertQuad()中提出的问题。
void TextureAtlas::insertQuads(V3F_C4B_T2F_Quad* quads, ssize_t index, ssize_t amount)
{
CCASSERT(index>=0 && amount>=0 && index+amount<=_capacity, “insertQuadWithTexture: Invalid index + amount”);

    _totalQuads += amount;

    CCASSERT( _totalQuads <= _capacity, "invalid totalQuads");

    // issue #575. index can be > totalQuads
    auto remaining = (_totalQuads-1) - index - amount;

    // last object doesn't need to be moved
    if( remaining > 0)
    {
        // tex coordinates
        memmove( &_quads,&_quads, sizeof(_quads) * remaining );
    }

    // 将提供的quads中amount个quad插入TextureAtlas::_quads中。
    auto max = index + amount;
    int j = 0;
    for (ssize_t i = index; i < max ; i++)
    {
        _quads = quads;
        index++;
        j++;
    }

    _dirty = true;
}

void TextureAtlas::insertQuadFromIndex(ssize_t oldIndex, ssize_t newIndex)
{
    // 这里的限制可以看出,不是在存储能力中随意移动,而是在已存储quads的范围内随意移动。
    CCASSERT( newIndex >= 0 && newIndex < _totalQuads, "insertQuadFromIndex:atIndex: Invalid index");
    CCASSERT( oldIndex >= 0 && oldIndex < _totalQuads, "insertQuadFromIndex:atIndex: Invalid index");

    if( oldIndex == newIndex )
    {
        return;    // 如果起始位置和目的位置相同,就不白费劲了。
    }
    // 由于abs()在iphone中的返回值不确定,所以这里自己实现abs()的功能。
    /* 计算出oldIndex与newIndex之间有多少个quads受影响。
     * 如果oldIndex在前,newIndex在后,则将受影响的quads向前移动一个位置。
     * 如果newIndex在前,oldIndex在后,则将受影响的quads向后移动一个位置。
     * (将newIndex空出来)
     */
    auto howMany = (oldIndex - newIndex) > 0 ? (oldIndex - newIndex) :  (newIndex - oldIndex);
    auto dst = oldIndex;
    auto src = oldIndex + 1;
    if( oldIndex > newIndex)
    {
        dst = newIndex+1;
        src = newIndex;
    }

    // texture coordinates
    V3F_C4B_T2F_Quad quadsBackup = _quads;    // 保存待移动的quad。
    memmove( &_quads,&_quads, sizeof(_quads) * howMany );    // 将newIndex空出来。
    _quads = quadsBackup;    // 待移动的quad放到newIndex处。

    _dirty = true;    // VBO需要更新。
}

(5) void removeQuadAtIndex(ssize_t index);

删除index处的quad。


void removeQuadsAtIndex(ssize_t index, ssize_t amount);

删除从index处开始amount个quads。


void removeAllQuads();

删除所有的quads。

index:指定删除的起始位置。

amount:指定删除的数量。

实现源码:

void TextureAtlas::removeQuadAtIndex(ssize_t index)
{
    CCASSERT( index>=0 && index<_totalQuads, "removeQuadAtIndex: Invalid index");    // 删除已存储的quads中的某一个。

    auto remaining = (_totalQuads-1) - index;    // 删除的quad后面是否还有其他quads。

    if( remaining )    // 如果还有其他quads。
    {
        // 将这些quads向前移动一个位置。
        memmove( &_quads,&_quads, sizeof(_quads) * remaining );
    }

    _totalQuads--;    // 存储的quad总数减1。

    _dirty = true;    // VBO需要更新。
}

// 与TextureAtlas::removeQuadAtIndex()的实现类似,多了个amount。
void TextureAtlas::removeQuadsAtIndex(ssize_t index, ssize_t amount)
{
    CCASSERT(index>=0 && amount>=0 && index+amount<=_totalQuads, "removeQuadAtIndex: index + amount out of bounds");

    auto remaining = (_totalQuads) - (index + amount);

    _totalQuads -= amount;

    if ( remaining )
    {
        memmove( &_quads, &_quads, sizeof(_quads) * remaining );
    }

    _dirty = true;
}

void TextureAtlas::removeAllQuads()
{
    _totalQuads = 0;    // 将存储的quads数量清零(实际的quads数据还存储在quad数组中)。
}

(6) bool resizeCapacity(ssize_t capacity);

重新设置TextureAtlas存储quads的能力。新的_capacity可大可小,不过如果重新设置失败,会将TextureAtlas::_capacity置0。

capacity:新的存储能力。

实现源码:

bool TextureAtlas::resizeCapacity(ssize_t newCapacity)
{
    CCASSERT(newCapacity>=0, "capacity >= 0");    // 新的存储能力不能是负值。
    if( newCapacity == _capacity )
    {
        return true;    // 如果新的存储能力和原来的一样,就不白费劲了。
    }
    auto oldCapactiy = _capacity;    // 保存旧的存储能力。
    // 这里这样写是为了如果新的存储能力变小了,存储的quads总数应与新的存储能力保持一致,多出的那些quads将被遗弃。
    _totalQuads = MIN(_totalQuads, newCapacity);
    _capacity = newCapacity;    // 存储能力设定为新的值。

    V3F_C4B_T2F_Quad* tmpQuads = nullptr;
    GLushort* tmpIndices = nullptr;
    
    // 根据新的存储能力为TextureAtlas::_quads分配空间。
    if (_quads == nullptr)    // 如果TextureAtlas::_quads之前没有被分配过空间(TextureAtlas::initWithTexture()中capacity指定为0就会造成这种情况)。
    {
        // 分配空间并清零。
        tmpQuads = (V3F_C4B_T2F_Quad*)malloc( _capacity * sizeof(_quads) );
        if (tmpQuads != nullptr)
        {
            memset(tmpQuads, 0, _capacity * sizeof(_quads) );
        }
    }
    else    // 如果TextureAtlas::_quads之前已经被分配过空间。
    {
        tmpQuads = (V3F_C4B_T2F_Quad*)realloc( _quads, sizeof(_quads) * _capacity );    // 对TextureAtlas::_quads的空间重分配。
        if (tmpQuads != nullptr && _capacity > oldCapactiy)    // 如果存储能力变大。
        {
            // 则对变大的部分清零。
            memset(tmpQuads+oldCapactiy, 0, (_capacity - oldCapactiy)*sizeof(_quads) );
        }
        _quads = nullptr;
    }

    // TextureAtlas::_indices与TextureAtlas::_quads同理。
    if (_indices == nullptr)
    {    
        tmpIndices = (GLushort*)malloc( _capacity * 6 * sizeof(_indices) );
        if (tmpIndices != nullptr)
        {
            memset( tmpIndices, 0, _capacity * 6 * sizeof(_indices) );
        }
    }
    else
    {
        tmpIndices = (GLushort*)realloc( _indices, sizeof(_indices) * _capacity * 6 );
        if (tmpIndices != nullptr && _capacity > oldCapactiy)
        {
            memset( tmpIndices+oldCapactiy, 0, (_capacity-oldCapactiy) * 6 * sizeof(_indices) );
        }
        _indices = nullptr;
    }

    // 如果quads和indices任意一个分配空间失败,则做相应的清理工作。
    if( ! ( tmpQuads && tmpIndices) ) {
        CCLOG("cocos2d: TextureAtlas: not enough memory");
        CC_SAFE_FREE(tmpQuads);
        CC_SAFE_FREE(tmpIndices);
        CC_SAFE_FREE(_quads);
        CC_SAFE_FREE(_indices);
        _capacity = _totalQuads = 0;    // 存储能力以及当前存储的quad数量会被置0。
        return false;
    }

    // TextureAtlas::_indices和TextureAtlas::_quads得到新的空间。
    _quads = tmpQuads;
    _indices = tmpIndices;

    // 像TextureAtlas::initWithTexture()那样重新初始化indices、申请VAO、VBO、IBO并初始化等等。
    setupIndices();
    mapBuffers();

    _dirty = true;    // VBO需要更新。

    return true;
}

(7) void moveQuadsFromIndex(ssize_t oldIndex, ssize_t amount, ssize_t newIndex);

将从oldIndex处开始的amount个quads移动到newIndex处,受影响的quads向前或向后移动。

oldIndex:待移动quads的起始位置。

amount:移动多少个quad。

newIndex:待移动quads的目的位置。

实现源码:

void TextureAtlas::moveQuadsFromIndex(ssize_t oldIndex, ssize_t amount, ssize_t newIndex)
{
    CCASSERT(oldIndex>=0 && amount>=0 && newIndex>=0, "values must be >= 0");
    CCASSERT(newIndex + amount <= _totalQuads, "insertQuadFromIndex:atIndex: Invalid index");
    CCASSERT(oldIndex < _totalQuads, "insertQuadFromIndex:atIndex: Invalid index");

    if( oldIndex == newIndex )
    {
        return;
    }
    //create buffer
    size_t quadSize = sizeof(V3F_C4B_T2F_Quad);
    V3F_C4B_T2F_Quad* tempQuads = (V3F_C4B_T2F_Quad*)malloc( quadSize * amount);
    memcpy( tempQuads, &_quads, quadSize * amount );

    if (newIndex < oldIndex)
    {
        // move quads from newIndex to newIndex + amount to make room for buffer

/* 这里注释是对的,编码是错的,应为:
* memmove(&_quads, &_quads, (oldIndex - newIndex)*quadSize);
*/
memmove( &_quads, &_quads, (oldIndex-newIndex)*quadSize);
}
else
{
// move quads above back
memmove( &_quads, &_quads, (newIndex-oldIndex)quadSize);
}
memcpy( &_quads, tempQuads, amount
quadSize);

    free(tempQuads);

    _dirty = true;
}

关键点总结:

♂ 该函数与TextureAtlas::insertQuadFromIndex()的功能类似,只不过多了个amount的概念,为什么不按照TextureAtlas::insertQuadFromIndex()的实现方式实现?比如:
void TextureAtlas::moveQuadsFromIndex(ssize_t oldIndex, ssize_t amount, ssize_t newIndex)
{
CCASSERT(newIndex >= 0 && newIndex + amount <= _totalQuads, “moveQuadsFromIndex:atIndex: Invalid index”);
CCASSERT(amount >= 0, “values must be >= 0”);
CCASSERT(oldIndex >= 0 && oldIndex < _totalQuads, “moveQuadsFromIndex:atIndex: Invalid index”);

   if( oldIndex == newIndex )
   {
       return;
   }
   // because it is ambiguous in iphone, so we implement abs ourselves
   // unsigned int howMany = abs( oldIndex - newIndex);
   auto howMany = (oldIndex - newIndex) > 0 ? (oldIndex - newIndex) :  (newIndex - oldIndex);
   auto dst = oldIndex;
   auto src = oldIndex + amount;
   if( oldIndex > newIndex)
   {
       dst = newIndex + amount;
       src = newIndex;
   }

   // texture coordinates
   V3F_C4B_T2F_Quad quadsBackup = {0};
   memcpy( quadsBackup, &_quads, sizeof(_quads) * amount );
   memmove( &_quads,&_quads, sizeof(_quads) * howMany );
   memcpy( &_quads, quadsBackup, amount * sizeof(_quads));

   _dirty = true;

}

(8) void increaseTotalQuadsWith(ssize_t amount);

单纯的增加TextureAtlas存储的quads数量。

amount:增加多少个quads。


void moveQuadsFromIndex(ssize_t index, ssize_t newIndex);

将从index开始之后的quads移动到newIndex处(覆盖)。

index:待移动quads的起始位置。

newIndex:待移动quads的目标位置。


void fillWithEmptyQuadsFromIndex(ssize_t index, ssize_t amount);

从index开始的amount个quads用空的quad填充(覆盖)。

index:待填充空quad的起始位置。

amount:填充多少个空quad。

实现源码:

void TextureAtlas::increaseTotalQuadsWith(ssize_t amount)
{
    CCASSERT(amount>=0, "amount >= 0");    // 增量不能为负数。
    _totalQuads += amount;    // 单纯的增加TextureAtlas存储的quads数量。
}

void TextureAtlas::moveQuadsFromIndex(ssize_t index, ssize_t newIndex)
{
    // 起始位置和目标为值不能超出能力范围。

// 觉得这里限制条件有缺失,index和newIndex也不能超过TextureAtlas::_capacity。
CCASSERT(index>=0 && newIndex>=0, “values must be >= 0”);
CCASSERT(newIndex + (_totalQuads - index) <= _capacity, “moveQuadsFromIndex move is out of bounds”);

    // 单纯的将index之后的quads覆盖到newIndex处。
    memmove(_quads + newIndex,_quads + index, (_totalQuads - index) * sizeof(_quads));
}

void TextureAtlas::fillWithEmptyQuadsFromIndex(ssize_t index, ssize_t amount)
{
    CCASSERT(index>=0 && amount>=0, "values must be >= 0");
    V3F_C4B_T2F_Quad quad;
    memset(&quad, 0, sizeof(quad));    // 空的quad。

    auto to = index + amount;
    for (ssize_t i = index ; i < to ; i++)
    {
        _quads i ] = quad;    // 从index开始填充amount个空quad。
    }
}

关键点总结:

♂ 这三个函数均是供class ParticleBatchNode使用的,具体为何要如此使用,在分析ParticleBatchNode时再深究。

(9) void drawQuads();
void drawNumberOfQuads(ssize_t n);
void drawNumberOfQuads(ssize_t numberOfQuads, ssize_t start);

渲染quads。

n,numberOfQuads:渲染多少个quads。

start:从哪个quad开始渲染。

实现源码:

// 渲染所有quads。
void TextureAtlas::drawQuads()
{
    this->drawNumberOfQuads(_totalQuads, 0);    // 从第一个quad开始,有多少个quad就渲染多少个。
}

// 从第一个quad开始,渲染numberOfQuads个quads。
void TextureAtlas::drawNumberOfQuads(ssize_t numberOfQuads)
{
    CCASSERT(numberOfQuads>=0, "numberOfQuads must be >= 0");    // 渲染的数量不能为负数。
    this->drawNumberOfQuads(numberOfQuads, 0);
}

// 从第start个quad开始,渲染numberOfQuads个quads。
void TextureAtlas::drawNumberOfQuads(ssize_t numberOfQuads, ssize_t start)
{
    // 渲染的起始位置和渲染的数量不能为负数。

    // 这里不再增加个限制条件,他们本身以及他们之和不能大于TextureAtals::_totalQuads?
    CCASSERT(numberOfQuads>=0 && start>=0, "numberOfQuads and start must be >= 0");

    if(!numberOfQuads)    // 如果渲染0个quad就不白费劲了。
        return;

    GL::bindTexture2D(_texture->getName());    // 绑定TextureAtlas::_texture(告诉OpenGL)。

    if (Configuration::getInstance()->supportsShareableVAO())    // 如果引擎配置为支持VAO。
    {
        //
        // Using VBO and VAO
        //

        // FIXME:: update is done in draw... perhaps it should be done in a timer
        if (_dirty)    // VBO需要更新。
        {
            glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO);    // 绑定VBO。
            // option 1: subdata
//            glBufferSubData(GL_ARRAY_BUFFER, sizeof(_quads)*start, sizeof(_quads) * n , &_quads );

            // option 2: data
//            glBufferData(GL_ARRAY_BUFFER, sizeof(quads_) * (n-start), &quads_, GL_DYNAMIC_DRAW);

            // option 3: orphaning + glMapBuffer
            // 先清空VBO,然后使用glMapBuffer将存储的quads更新至VBO。
            glBufferData(GL_ARRAY_BUFFER, sizeof(_quads) * _capacity, nullptr, GL_DYNAMIC_DRAW);
            void *buf = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
            memcpy(buf, _quads, sizeof(_quads)* _totalQuads);
            glUnmapBuffer(GL_ARRAY_BUFFER);
            
            glBindBuffer(GL_ARRAY_BUFFER, 0);    // 解绑VBO。

            _dirty = false;    // VBO更新完成,标志位置为false。
        }

        GL::bindVAO(_VAOname);    // 绑定VAO。

#if CC_REBIND_INDICES_BUFFER
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO);    // 绑定IBO。
#endif

        // 渲染quads。以三角形的形式绘制,这样绘制一个方形图片需要绘制两个三角形,所以需要6个顶点。这里还有个start,说明从索引序列中的哪个索引开始使用。
        glDrawElements(GL_TRIANGLES, (GLsizei) numberOfQuads*6, GL_UNSIGNED_SHORT, (GLvoid*) (start*6*sizeof(_indices)) );

#if CC_REBIND_INDICES_BUFFER
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);    // 解绑IBO。
#endif

//    glBindVertexArray(0);
    }
    else    // 引擎配置为只支持VBO。
    {
        //
        // Using VBO without VAO
        //

#define kQuadSize sizeof(_quads.bl)
        glBindBuffer(GL_ARRAY_BUFFER, _buffersVBO);    // 绑定VBO。

        // FIXME:: update is done in draw... perhaps it should be done in a timer
        if (_dirty)    // VBO需要更新。
        {
            // 使用glBufferSubData将存储的quads更新至VBO。
            glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(_quads) * _totalQuads , &_quads );
            _dirty = false;    // VBO更新完成,标志位置为false。
        }

        GL::enableVertexAttribs(GL::VERTEX_ATTRIB_FLAG_POS_COLOR_TEX);

        // 初始化3D坐标信息。
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, vertices));

        // 初始化颜色信息。
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, colors));

        // 初始化UV坐标信息。
        glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, kQuadSize, (GLvoid*) offsetof(V3F_C4B_T2F, texCoords));

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _buffersVBO);  // 绑定IBO。

        // 渲染quads。以三角形的形式绘制,这样绘制一个方形图片需要绘制两个三角形,所以需要6个顶点。这里还有个start,说明从索引序列中的哪个索引开始使用。
        glDrawElements(GL_TRIANGLES, (GLsizei)numberOfQuads*6, GL_UNSIGNED_SHORT, (GLvoid*) (start*6*sizeof(_indices)));

        glBindBuffer(GL_ARRAY_BUFFER, 0);   // 解绑VBO。
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);   // 解绑IBO。
    }

    /* 更新调试信息。就是程序运行后左下角那个“GL verts”(向OpenGL传递了多少个顶点信息)
     * 和“GL calls”(向OpenGL发送了多少次渲染命令)。
     * 这里向OpenGL发送了1次渲染命令,传递了6个顶点的信息。
     */
    CC_INCREMENT_GL_DRAWN_BATCHES_AND_VERTICES(1,numberOfQuads*6);

    CHECK_GL_ERROR_DEBUG();    // 检查OpenGL是否返回了错误。
}

关键点总结:

♂ glMapBuffer的使用可参考:http://book.51cto.com/art/201002/185473.htm。

(10) inline bool isDirty(void)
inline void setDirty(bool bDirty)

返回TextureAtlas::_dirty标志位的值;更新TextureAtlas::_dirty标志位的值。

bDirty:待更新的TextureAtlas::_dirty标志位的值。

(11) virtual std::string getDescription() const;

格式化打印TextureAtlas::_totalQuads的值。

(12) ssize_t getTotalQuads() const;
ssize_t getCapacity() const;
Texture2D* getTexture() const;
void setTexture(Texture2D* texture);
V3F_C4B_T2F_Quad* getQuads();
void setQuads(V3F_C4B_T2F_Quad* quads);

获取或是设置一些成员变量的值。

实现源码:

大部分实现源码都很简单就不再赘述了,只说明部分几个。

void TextureAtlas::setTexture(Texture2D * var)
{
    CC_SAFE_RETAIN(var);    // 纹理可能在之后仍会使用,在这里retain。
    CC_SAFE_RELEASE(_texture);    // 在TextureAtlas::initWithTexture()中SAFE_RETAIN过,现在可以释放所有权了。
    _texture = var;    // 更新为新的纹理。
}

V3F_C4B_T2F_Quad* TextureAtlas::getQuads()
{
    _dirty = true;    // 如果有人调用该函数,就假设quads数组被更改过,需要更新VBO。为何是这种假设不太明白。
    return _quads;    // 返回quads数组。
}

void TextureAtlas::setQuads(V3F_C4B_T2F_Quad* quads)
{
    _quads = quads;    // 更新为新的quads数组。

    // 原先的quads数组不用释放?
}