本文给大家介绍下在cocos2dx中怎样把texture保存为pvr或者pvr.ccz格式的文件
pvr格式的数据在IOS上直接交给显卡渲染的,而cocos2dx 中的texture是直接交给显卡渲染的,所以理论上将pvr格式的数据可以不进行任何数据的转换就可以生成一张texture,事实上确实是这样的。
一, 保存为pvr格式
要保存pvr文件,我们首先分析cocos2dx引擎中是怎样解析pvr文件的。可以看到cocos2dx库中的CCTexturePVR类提供了两个方法来解析pvr格式图片分别是unpackPVRv2Data、unpackPVRv3Data,两个方法分别解析的是PVR的v2和v3版本。本文只分析v2版本:
bool CCTexturePVR::unpackPVRv2Data(unsigned char* data, unsigned int len)//data是直接从文件里读出来没有做任何处理的数据 { bool success = false; ccPVRv2TexHeader *header = NULL; unsigned int flags, pvrTag; unsigned int dataLength = 0, dataOffset = 0, dataSize = 0; unsigned int blockSize = 0, widthBlocks = 0, heightBlocks = 0; unsigned int width = 0, height = 0, bpp = 4; unsigned char *bytes = NULL; unsigned int formatFlags; // 用header指向data数据的头部(PVR v2格式的头部固定是52个字节,也就是data的前52个字节的数据,data剩下的数据就是生成texture的数据了) header = (ccPVRv2TexHeader *)data; // 这是头部的一个标识值为 "PVR!" 占4字节 pvrTag = CC_SWAP_INT32_LITTLE_TO_HOST(header->pvrTag); if (gPVRTexIdentifier[0] != (char)(((pvrTag >> 0) & 0xff)) || gPVRTexIdentifier[1] != (char)(((pvrTag >> 8) & 0xff)) || gPVRTexIdentifier[2] != (char)(((pvrTag >> 16) & 0xff)) || gPVRTexIdentifier[3] != (char)(((pvrTag >> 24) & 0xff))) { return false; } CCConfiguration *configuration = CCConfiguration::sharedConfiguration(); flags = CC_SWAP_INT32_LITTLE_TO_HOST(header->flags); formatFlags = flags & PVR_TEXTURE_FLAG_TYPE_MASK; bool flipped = (flags & kPVR2TextureFlagVerticalFlip) ? true : false; if (flipped) { CCLOG("cocos2d: WARNING: Image is flipped. Regenerate it using PVRTexTool"); } if (! configuration->supportsNPOT() && (header->width != ccNextPOT(header->width) || header->height != ccNextPOT(header->height))) { CCLOG("cocos2d: ERROR: Loading an NPOT texture (%dx%d) but is not supported on this device", header->width, header->height); return false; } unsigned int pvr2TableElements = PVR2_MAX_TABLE_ELEMENTS; if (! CCConfiguration::sharedConfiguration()->supportsPVRTC()) { pvr2TableElements = 9; } for (unsigned int i = 0; i < pvr2TableElements; i++) { //Does image format in table fits to the one parsed from header? if (v2_pixel_formathash[i].pixelFormat == formatFlags) { m_pPixelFormatInfo = v2_pixel_formathash[i].pixelFormatInfo; //Reset num of mipmaps m_uNumberOfMipmaps = 0; //Get size of mipmap m_uWidth = width = CC_SWAP_INT32_LITTLE_TO_HOST(header->width); m_uHeight = height = CC_SWAP_INT32_LITTLE_TO_HOST(header->height); //Do we use alpha ? if (CC_SWAP_INT32_LITTLE_TO_HOST(header->bitmaskAlpha)) { m_bHasAlpha = true; } else { m_bHasAlpha = false; } //Get ptr to where data starts.. dataLength = CC_SWAP_INT32_LITTLE_TO_HOST(header->dataLength); //跳过头部,bytes直接指向了图片数据部分 bytes = ((unsigned char *)data) + sizeof(ccPVRv2TexHeader); m_eFormat = m_pPixelFormatInfo->ccPixelFormat; bpp = m_pPixelFormatInfo->bpp; // Calculate the data size for each texture level and respect the minimum number of blocks while (dataOffset < dataLength) { switch (formatFlags) { case kPVR2TexturePixelFormat_PVRTC_2BPP_RGBA: blockSize = 8 * 4; // Pixel by pixel block size for 2bpp widthBlocks = width / 8; heightBlocks = height / 4; break; case kPVR2TexturePixelFormat_PVRTC_4BPP_RGBA: blockSize = 4 * 4; // Pixel by pixel block size for 4bpp widthBlocks = width / 4; heightBlocks = height / 4; break; case kPVR2TexturePixelFormat_BGRA_8888: if (CCConfiguration::sharedConfiguration()->supportsBGRA8888() == false) { CCLOG("cocos2d: TexturePVR. BGRA8888 not supported on this device"); return false; } default: blockSize = 1; widthBlocks = width; heightBlocks = height; break; } // Clamp to minimum number of blocks if (widthBlocks < 2) { widthBlocks = 2; } if (heightBlocks < 2) { heightBlocks = 2; } dataSize = widthBlocks * heightBlocks * ((blockSize * bpp) / 8); unsigned int packetLength = (dataLength - dataOffset); packetLength = packetLength > dataSize ? dataSize : packetLength; // bytes指向的是data,而data是直接从文件里面读出来的数据,m_asMipmaps则是用来生成texture的数据, // 所以这里可以得出结论,pvr的图片数据到texture不需要经过任何转换, // 也就是说pvr格式的数据可以不进行任何数据的转换就可以生成一张texture m_asMipmaps[m_uNumberOfMipmaps].address = bytes + dataOffset; m_asMipmaps[m_uNumberOfMipmaps].len = packetLength; m_uNumberOfMipmaps++; //Check that we didn‘t overflow CCAssert(m_uNumberOfMipmaps < CC_PVRMIPMAP_MAX, "TexturePVR: Maximum number of mipmaps reached. Increase the CC_PVRMIPMAP_MAX value"); dataOffset += packetLength; //Update width and height to the next lower power of two width = MAX(width >> 1, 1); height = MAX(height >> 1, 1); } //Mark pass as success success = true; break; }
通过上面的分析,我们知道了要把texture保存成pvr格式的文件只需为这样texture添加一个头部即可。关于pvr头部在CCTexturePVR.cpp中我们可以看到 _PVRTexHeader 这个结构体,这个结构体就是PVR的头部格式,现在要做的工作就是分析出_PVRTexHeader的成员的含义。这个可以参考imageination关于pvr的文档,需要提醒的是要注意版本差别。这里截出了头部描述的部分:
根据表格的解析,PVR的头部就比较容易写出了。
具体步骤: 1. 设置头部数据
2. 把头部数据和texture数据合并
3. 输出
这是我在CCImage中添加的保存PVR格式的方法,大家可以参考下:
bool CCImage::_saveImageToPVRCCZ(const char *pszFilePath) { bool bRet = false; do { CC_BREAK_IF(NULL == pszFilePath); unsigned int *pvrHeader = new unsigned int[52]; // pvr header V2 pvrHeader[0] = 52; //headerLength pvrHeader[1] = m_nHeight; // height 变量是CCImage中的成员变量 pvrHeader[2] = m_nWidth; //width 变量是CCImage中的成员变量 pvrHeader[3] = 0; //numMipmaps pvrHeader[4] = 0x8012;//flags pvrHeader[5] = 0; //dataLength pvrHeader[6] = 0x20; // bpp = 32 // R G B A 是连续的4个字节,每个自街上都赋值为0xff表明存在活使用该颜色值(如果不使用A通道直接把A通道的值置为0x0即可) //0xff ff ff ff pvrHeader[7] = 0xff; // bitmaskRed pvrHeader[8] = 0xff00; // bitmaskGreen G pvrHeader[9] = 0xff0000; // bitmaskBlue B pvrHeader[10] = 0xff000000; // bitmaskAlpha A pvrHeader[11] = 0x21525650; // pvrTag = PVR! pvrHeader[12] = 1; // numSurfs unsigned int blockSize = 1; // default pvrHeader[5] = m_nWidth * m_nHeight * ((blockSize * 32) / 8); unsigned long pCombineDataLength = m_nWidth * m_nHeight * 4 + 52; unsigned char *pCombineData = new unsigned char[pCombineDataLength]; // header memcpy(pCombineData, (unsigned char *)pvrHeader, 52); CC_SAFE_DELETE_ARRAY(pvrHeader); // body memcpy(pCombineData + 52, m_pData, m_nWidth * m_nHeight * 4); //m_pData就是用来渲染纹理的数据(pvr除了头部的那部分数据) m_bPreMulti = true; // pCombineData就是一个完成的pvr格式的数据了,在此处输出pCombineData即可 if (!ret) { CCLOG("cocos2d: CompressPvrToCCZFile failed!"); CC_SAFE_DELETE_ARRAY(pCombineData); return false; } CC_SAFE_DELETE_ARRAY(pCombineData); bRet = true; } while (0); return bRet; }
二,保存为pvr.ccz格式
那么现在来分析保存为PVR.CCZ格式。我们可以跟踪一张pvr.ccz格式的图片,观察其是怎样生成texture的,最终发现在CCTexturePVR类的initWithContentsOfFile方法中可以看到这段语句
if (lowerCase.find(".ccz") != std::string::npos) { pvrlen = ZipUtils::ccInflateCCZFile(path, &pvrdata); }
除此之外pvr.ccz 和pvr格式的处理过程是一模一样的。ccInflateCCZFile相当于解压了pvr.ccz文件并把解压后的数据保存到了pvrdata中。所以要把pvr转成pvr.ccz我们需要先把pvr数据用ZipUtils库压缩,再为这个数据添加一个头部即可。ZipUtils.h定义了一个CCZHeader,这个就是pvr.ccz的头部格式,pvr.ccz的头部比较简单,只有4个字段,这4个字段的数据都可以从ccInflateCCZFile中了解到,这里及不再赘述了,具体步骤如下:
1. 先把pvr数据压缩,利用ZipUtils的compress方法压缩后得到压缩后的数据outBuffer
2. 为outBuffer添加一个头部:
CCZHeader header = {{‘C‘, ‘C‘, ‘Z‘, ‘!‘}, CCZ_COMPRESSION_ZLIB , 256, 0, CC_SWAP_INT32_BIG_TO_HOST(inLength)};
注意:inLength是pvr数据的长度,这里必须转换为大端的表示方法。(在ccInflateCCZFile中可以看到把这个长度转换为小端)
3. 把两个数据一次输出到同一个文件即可。代码如下(此处参考了http://www.cnblogs.com/howeho/p/3586379.html):
bool ZipUtils::ccCompressPvrToCCZFile(unsigned char *inBuffer,unsigned long inLength, const char *pszFileName)// inBuffer是pvr数据,inLength是pvr数据的长度 { bool bRet = false; do { if (NULL == pszFileName) { CCLOG("cocos2d: Error pszFileName NULL!"); } if(NULL == inBuffer || 0 == inLength) { CCLOG("cocos2d: Error argument inBuffer or inLength"); return false; } unsigned long bufferSize = inLength; char* outBuffer=new char[(uInt)bufferSize]; memset(outBuffer, 0, bufferSize); int ret = compress((Bytef*)outBuffer,(uLongf*)&bufferSize,(const Bytef*)inBuffer,(uLongf)inLength); if (ret != Z_OK) { CCLOG("cocos2d: Failed to compress data"); CC_SAFE_DELETE_ARRAY(outBuffer); return false; } CCZHeader header = {{‘C‘, ‘C‘, ‘Z‘, ‘!‘}, CCZ_COMPRESSION_ZLIB , 256, 0, CC_SWAP_INT32_BIG_TO_HOST(inLength)}; // writeFileData 是我在CCFIleUtils中自己添加的写文件的方法,大家可以自己添加或者直接在此处输出 CCFileUtils::sharedFileUtils()->writeFileData(pszFileName, "w", (const char*)&header, sizeof(header)); CCFileUtils::sharedFileUtils()->writeFileData(pszFileName, "ab+", (const char*)outBuffer, bufferSize); CC_SAFE_DELETE_ARRAY(outBuffer); bRet = true; } while (0); return bRet; }