Cocos2dx在Android上使用ETC1+Alpha压缩纹理

我们为了优化游戏的内存占用,会给图片资源进行有损压缩,在Android上则是使用ETC1(Ericsson texture compression)进行纹理压缩,压缩纹理无论从加载速度(GPU识别)和内存占用都有很大的优势,唯一的缺点就是有损。
也就是它不是万金油,并不是所有的图片都能使用ETC1压缩。我在记录下我是如何在Cocos2dx中使用ETC1进行纹理压缩.当然这里是在android平台下使用。

为什么是ETC1

ETC1格式是OpenGL ES图形标准的一部分,并且被所有的Android设备所支持,不支持透明通道。需要是POT纹理。虽然后面的ETC2支持透明通道,但是它是OpenGL 3.0的标准,并不能被所有Android设备所支持,而ETC1我们能通过技术手段加入透明通道。参考这篇文章

之前的准备

ETC1

我采用将一张纹理分割成两张图的方案,也就是图片 = RGB部分纹理+Alpha部分纹理。因为纹理大小由于硬件和操作系统原因是有限制的,2048x2048基本能被主流设备所认同,如果采用Alpha拼接的方式,原本2048的纹理最终大小会超过2048,如果所有纹理加上最大尺寸1024的限制又会使纹理数量增多.所以最终我选择了分割图片的方案。

ImageMagick

使用ImageMagick来分割纹理的RGB和Alpha部分,为什么没有用Mali GPU Texture Compression直接生成呢?因为它生成的最终的pkm文件是经过压缩的,压缩率并不理想。所以后面我会介绍我使用zlib来压缩生成的ETC1格式的纹理。

分离RGB部分

1
convert logo.png -alpha Off logo_rgb.png

分离Alpha部分

1
convert logo.png -channel A -alpha extract logo_a.png

PVRTexTool

使用PVRTexTool压缩ETC1纹理,注意这里生成的文件的后缀是pvr,其实它的格式是ETC1

1
2
PVRTexTool -f ETC1 -i logo_rgb.png -o logo_rgb.pvr -q etcfast
PVRTexTool -f ETC1 -i logo_a.png -o logo_a.pvr -q etcfast

现在logo_rgb.pvrlogo_a.pvr已经是我们需要的ETC1格式的纹理了,但是你会发现它们比转换之前的文件大小大了很多:(
不能增加我们的包大小是不?所以我们先使用zlib来压缩下他们,为什么使用zlib? 因为2dx里已经有zlib库(记得iOS里的xx.pvrxx.pvr.ccz吧,ccz其实就被zlib压缩过后的PVRTC4纹理),我们不用引入其他库,偷个懒:),当然我们也可以使用其他压缩算法,比如梦幻西游。听他们的开发说,使用的是lzma解压资源,但是它的解压速度会稍慢,但是压缩率比较高,这就需要你自己取舍了。

压缩纹理

我们需要一个工具,他能将纹理使用zlib压缩成一个2dx能识别的压缩格式,或者我们能在代码里能识别的文件.

我们可以仿照pvr.ccz的策略,修改我们最终生成压缩文件的文件头信息,告诉2dx使用zlib来解压它。定义一个8个字节的结构体,表示我们的头信息.cpp中的结构体是连续的内存分配:)

1
2
3
4
5
struct ZipHeaderInfo
{
char sig[4];
int fileSize;
};

前四个char分别为!,E,T,C,后面的int用来存储文件的原始大小。

最终的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//
// CompressETCTexture
//
// Created by DannyHe on 9/16/15.
// Copyright (c) 2015 DannyHe. All rights reserved.
//
#include "ETCCompress.h"
#include <iostream>
#include <stdlib.h>
using namespace std;
struct ZipHeaderInfo
{
char sig[4];
int fileSize;
};
int ETCCompress::compressETC(const char * destpath,const char *srcpath)
{
ZipHeaderInfo zipHeader;
FILE* inFile = fopen(srcpath, "rb");
if(!inFile)
{
return -1;
}
fseek(inFile, 0, SEEK_END);
int fileSize = ftell(inFile);
char * fileData = new char[fileSize];
fseek(inFile, 0, SEEK_SET);
fread(fileData, 1, fileSize, inFile);
fclose(inFile);
zipHeader.fileSize = fileSize;
zipHeader.sig[0] = '!';
zipHeader.sig[1] = 'E';
zipHeader.sig[2] = 'T';
zipHeader.sig[3] = 'C';
uLongf destLength = compressBound(fileSize);
Bytef* pDestBuf = new Bytef[destLength];
int result = compress2(pDestBuf , &destLength, (const Bytef*)fileData, fileSize,9);
if (result != Z_OK)
{
switch(result)
{
case Z_MEM_ERROR:
printf("ETCCompress:: note enough memory for compression");
break;
case Z_BUF_ERROR:
printf("ETCCompress:: note enough room in buffer to compress the data");
break;
}
return -1;
}
cout << "ETCCompress:: orignal size: " << fileSize << " bytes"
<< " , compressed size : " << destLength << " bytes"
<< " , header size: " << sizeof(zipHeader) << " bytes"
<< " , final size : " << sizeof(zipHeader) + destLength << " bytes"
<< " , compress ratio:" << (1 - (double)(sizeof(zipHeader) + destLength)/fileSize)*100 << "%"
<< '\n';
FILE* fo = fopen(destpath, "wb+");
if(fo)
{
fwrite(&zipHeader, sizeof(zipHeader), 1, fo);
fwrite(pDestBuf,destLength, 1, fo);
fclose(fo);
delete [] pDestBuf;
return 0;
}
return 0;
}
uLongf ETCCompress::unCompressETC(const char * packData,int packSize,Bytef* &buff)
{
struct ZipHeaderInfo *header = (struct ZipHeaderInfo*) packData;
if (!(header->sig[0] == '!' && header->sig[1] == 'E' && header->sig[2] == 'T' && header->sig[3] == 'C')) {
printf("\nETCCompress:: header error");
return -1;
}
int orginSize = header->fileSize;
int headerSize = sizeof(*header);
uLongf newSize = orginSize;
Bytef* pUnBuf = new Bytef[newSize];
int result2 = uncompress(pUnBuf, &newSize,(const Bytef*)packData + headerSize,packSize - headerSize);
if (result2 != Z_OK)
{
switch(result2)
{
case Z_MEM_ERROR:
printf("ETCCompress:: note enough memory for uncompression");
break;
case Z_BUF_ERROR:
printf("ETCCompress:: note enough room in buffer to uncompress the data");
break;
}
return -1;
}
buff = pUnBuf;
cout << "orignal size: " << packSize << " bytes"
<< " , ucompressed size : " << orginSize << " bytes" << '\n';
return newSize;
}
int ETCCompress::unCompressETC(const char *destpath, const char *srcpath)
{
FILE* packFile = fopen(srcpath, "rb");
fseek(packFile, 0, SEEK_END);
int packSize = ftell(packFile);
char * packData = new char[packSize];
fseek(packFile, 0, SEEK_SET);
fread(packData, 1, packSize, packFile);
fclose(packFile);
Bytef* pUnBuf;
uLongf newSize = unCompressETC(packData,packSize,pUnBuf);
if (newSize == -1)
{
printf("\nETCCompress:: uncompress error!");
return -1;
}
FILE* ft = fopen(destpath, "wb+");
if(ft)
{
fwrite(pUnBuf,newSize, 1, ft);
fclose(ft);
delete [] pUnBuf;
return 0;
}
return -1;
}

更多详细的代码及编译可以看我之前的这篇文章仓库
然后我们使用我们写的工具压缩我们的纹理

1
2
CompressETCTexture pack logo_rgb.pvr logo_rgb.png
CompressETCTexture pack logo_a.pvr logo_a.png

最后我们得到两个文件logo_rgb.pnglogo_a.png,这两个文件经过了ETC1压缩并且文件大小也是我们能接受的范围,然后我们需要在Cocos2dx中使用他们。

2dx(3.x)中解压

我们在ZipUtils类中加入我们的解压逻辑
头文件ZipUtils.h中声明我们的头信息结构体和解压函数

1
2
3
4
5
6
7
8
9
10
11
#if CC_USE_ETC1_ZLIB
struct ETCCompressedHeader{
char sig[4];
int fileSize;
};
#endif
#if CC_USE_ETC1_ZLIB
static bool isETCCompressedBuffer(const unsigned char *buffer, ssize_t len);
static int inflateETCCompressedBuffer(const unsigned char *buffer, ssize_t len, unsigned char **out);
#endif

实现解压

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#if CC_USE_ETC1_ZLIB
bool ZipUtils::isETCCompressedBuffer(const unsigned char *buffer, ssize_t len)
{
if (static_cast<size_t>(len) < sizeof(struct ETCCompressedHeader))
{
return false;
}
struct ETCCompressedHeader *header = (struct ETCCompressedHeader*) buffer;
return header->sig[0] == '!' && header->sig[1] == 'E' && header->sig[2] == 'T' && header->sig[3] == 'C';
}
int ZipUtils::inflateETCCompressedBuffer(const unsigned char *buffer, ssize_t bufferLen, unsigned char **out)
{
struct ETCCompressedHeader *header = (struct ETCCompressedHeader*) buffer;
int len = header->fileSize;
*out = (unsigned char*)malloc( len );
if(! *out )
{
CCLOG("cocos2d: ETCCompressed: Failed to allocate memory for texture");
return -1;
}
uLongf destlen = len;
int ret = uncompress(*out, &destlen, (Bytef*)buffer + sizeof(*header), bufferLen - sizeof(*header) );
if( ret != Z_OK )
{
CCLOG("cocos2d: ETCCompressed: Failed to uncompress data");
free( *out );
*out = nullptr;
return -1;
}
return len;
}
#endif

最后我们在2dx读取纹理文件的地方(Image::initWithImageData)调用我们的解压函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#if CC_USE_ETC1_ZLIB
if(ZipUtils::isETCCompressedBuffer(data,dataLen))
{
CCLOG("Image: Use our etc format compressed!");
unsigned char* etcUnpackedData = nullptr;
ssize_t etcUnpackedLen = 0;
etcUnpackedLen = ZipUtils::inflateETCCompressedBuffer(data,dataLen,&etcUnpackedData);
//detecgt and unzip the compress file
if (ZipUtils::isCCZBuffer(etcUnpackedData, etcUnpackedLen))
{
unpackedLen = ZipUtils::inflateCCZBuffer(etcUnpackedData, etcUnpackedLen, &unpackedData);
}
else if (ZipUtils::isGZipBuffer(etcUnpackedData, etcUnpackedLen))
{
unpackedLen = ZipUtils::inflateMemory(const_cast<unsigned char*>(etcUnpackedData), etcUnpackedLen, &unpackedData);
}
else
{
unpackedData = const_cast<unsigned char*>(etcUnpackedData);
unpackedLen = etcUnpackedLen;
}
if(etcUnpackedData != unpackedData)
{
free(etcUnpackedData);
}
}
else
{
//detecgt and unzip the compress file
if (ZipUtils::isCCZBuffer(data, dataLen))
{
unpackedLen = ZipUtils::inflateCCZBuffer(data, dataLen, &unpackedData);
}
else if (ZipUtils::isGZipBuffer(data, dataLen))
{
unpackedLen = ZipUtils::inflateMemory(const_cast<unsigned char*>(data), dataLen, &unpackedData);
}
else
{
unpackedData = const_cast<unsigned char*>(data);
unpackedLen = dataLen;
}
}
#else
//detecgt and unzip the compress file
if (ZipUtils::isCCZBuffer(data, dataLen))
{
unpackedLen = ZipUtils::inflateCCZBuffer(data, dataLen, &unpackedData);
}
else if (ZipUtils::isGZipBuffer(data, dataLen))
{
unpackedLen = ZipUtils::inflateMemory(const_cast<unsigned char*>(data), dataLen, &unpackedData);
}
else
{
unpackedData = const_cast<unsigned char*>(data);
unpackedLen = dataLen;
}
#endif

2dx中使用ETC1

我使用Shader来操作最中的Alpha,比如在CCSprite中,发现自己使用的纹理是ETC1格式便去查找Alpha纹理,如果发现便使用自己的Shader替换默认的Shader.
这样就做到对游戏以前的开发逻辑毫无修改。因为运用Shader的方式比较多,我这里就只列出我的Shader代码(CCSprite)

顶点不需要修改默认的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const char* ccShader_etc1_PositionTextureColor_noMVP_vert = STRINGIFY(
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
void main()
{
gl_Position = CC_PMatrix * a_position;
v_fragmentColor = a_color;
v_texCoord = a_texCoord;
}
);

片段着色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// u_texture1是etc的alpha数据也可以用ETC1压缩
const char* ccShader_etc1_PositionTextureColor_noMVP_frag = STRINGIFY(
\n
varying vec4 v_fragmentColor;
varying vec2 v_texCoord;
uniform sampler2D u_texture1;
void main()
{
vec4 color = texture2D(CC_Texture0, v_texCoord);
color.a = texture2D(u_texture1, v_texCoord).r;
gl_FragColor = color * v_fragmentColor; //支持Cocos opacity
}
);

使用的话大概只需这样

1
2
3
4
auto program = GLProgramCache::getInstance()->getGLProgram(GLProgram::SHADER_NAME_ETC_ALPHA_POSITION_TEXTURE_COLOR_NO_MVP); //新加的etc shader
auto etc_program_state = GLProgramState::create(program);
etc_program_state->setUniformTexture("u_texture1", texture_alpha);
setGLProgramState(etc_program_state);

最后

如果还需要优化包大小,可以采用将PNG转换成两张JPG,也是RGB+ALPHA.刀塔传奇就有使用这种策略。我们还可以直接压缩png,使它的画质降低,比如pngquant

另外上文中我们zlib压缩文件的小工具也可以来压缩其他文件,比如我们在Windows Phone平台使用的压缩纹理DXT3
在发布Android的时候我们同样需要声明我们的游戏使用ETC压缩

1
2
<!-- we want the device support etc1 texture format -->
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />