【技术分享之三】cocos实现对ETC2的支持

ETC2简要介绍

etc1的问题是不支持透明通道;而pvr2的问题是透明图片质量太差,且图片大小必须是2的幂和正方形。etc2的出现正好弥补了这两个格式的不足。

etc2不仅兼容etc1,还支持透明通道,并且提供了更多的像素格式。etc2已经是OpenGL ES3.0的标准之一。也就是只要操作系统和硬件支持ES3.0,则必然支持ETC2,不管它是Android还是IOS。

目前市面上使用etc2作为压缩纹理的游戏不多,主要原因是老机器不支持,特别是安卓。而制约其流行起来的原因,其实就是两个:GPU的支持,OS的支持。

我从wikipedia上查了OpenGL ES 3.0的兼容情况,大概是这样的:

  • 软件:
    • android 4.3以上支持ES3.0
    • IOS 7以上支持ES3.0
  • 硬件:
    • Adreno 300 and 400 series (Android, BlackBerry 10, Windows Phone 8, Windows RT)
    • Mali T600 series onwards (Android, Linux, Windows 7)
    • PowerVR Series6 (iOS, Linux)
    • Vivante (Android, OS X 10.8.3, Windows 7)
    • Nvidia (Android, Linux, Windows 7)
    • Intel (Linux)
  • 苹果设备从A7开始支持ES3.0,最低要求的设备是:
    • iPhone 5S
    • iPad Air
    • iPad mini with Retina display

尽管苹果的开发文档说到:

OpenGL ES 3.0 also supports the ETC2 and EAC compressed texture formats; however, PVRTC textures are recommended on iOS devices.

然而我对PVRTC实在是爱不起来,又必须是2的幂,又必须是正方形,最终效果还那么差(4bit一个像素)。所以当A7支持ETC2之后,其实是可以考虑换用ETC2的,这样可以和安卓很好的统一起来。

改用ES3.0的EGLContext

如何在cocos中支持etc2,其实这件事由官方来做最好,可能他们考虑到设备的兼容性问题,就没有实现这个特性。好在支持这个并不困难,我就自己动手实现了。

cocos使用的是ES2.0的版本,经测试发现,安卓上如果硬件支持ETC2,context并不用换成3.0。而IOS就必须明确创建3.0的EGLContext,才可以使用ETC2。

minggo之前提交过一个PR支持GLES3,在这里,只需要把它修改的文件合并进项目即可,可以两个都合,也可以只合IOS的。

PKM2格式说明

ETC2只是一个压缩算法,还需要一种文件格式来包含它,etc1常包含在pkm文件中,etc2也可以在pkm中,只不过etc1的是pkm10版本,而etc2需要pkm20版本,这两个文件版本是兼容的,它的格式如下:

4 byte	magic number: "PKM "
2 byte 	version "10" or "20"
2 byte 	format: 0 (ETC1_RGB_NO_MIPMAPS), 1(ETC2_RGB_NO_MIPMAPS), ...
16 bit 	big endian extended width
16 bit 	big endian extended height
16 bit 	big endian original width
16 bit 	big endian original height
data, 64bit big endian words.

其中format字段:

// 常量                         		  值                对应的压缩纹理格式
// --------------------------------------------------------------------
// ETC1_RGB_NO_MIPMAPS                  0                 GL_ETC1_RGB8_OES
// ETC2_RGB_NO_MIPMAPS                  1                 GL_COMPRESSED_RGB8_ETC2
// ETC2_RGBA_NO_MIPMAPS_OLD             2, not used       -
// ETC2_RGBA_NO_MIPMAPS                 3                 GL_COMPRESSED_RGBA8_ETC2_EAC
// ETC2_RGBA1_NO_MIPMAPS                4                 GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2
// ETC2_R_NO_MIPMAPS                    5                 GL_COMPRESSED_R11_EAC
// ETC2_RG_NO_MIPMAPS                   6                 GL_COMPRESSED_RG11_EAC
// ETC2_R_SIGNED_NO_MIPMAPS             7                 GL_COMPRESSED_SIGNED_R11_EAC
// ETC2_RG_SIGNED_NO_MIPMAPS            8                 GL_COMPRESSED_SIGNED_RG11_EAC

我修改的时候只加了ETC2_RGB_NO_MIPMAPS和ETC2_RGBA_NO_MIPMAPS的支持,第一个是RGB,和ETC1兼容,一个像素占用4位;第二个是RGBA,提供透明通道,一个像素占用8位;这有点像以前贴子提到的ETC1+Alpha,好处是我们不用再写自定义Shader了。

修改Cocos引擎

上面预热了那么久,终于要修改引擎了,这么一步并不麻烦,我们只要依照ETC1的代码添加ETC2的代码就行:

修改Configuration,提供ETC2的支持判断

  • 增加_supportsETC2成员变量,用于判断是否支持ETC2
  • 增加函数checkForEtc2
bool Configuration::checkForEtc2() const
{
    // Only the following two formats are supported
#define GL_COMPRESSED_RGB8_ETC2           0x9274
#define GL_COMPRESSED_RGBA8_ETC2_EAC      0x9278

    GLint numFormats = 0;
    glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &numFormats);
    GLint* formats = new GLint[numFormats];
    glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS, formats);

    int supportNum = 0;
    for (GLint i = 0; i < numFormats; ++i)
    {
        if (formats[i] == GL_COMPRESSED_RGB8_ETC2 || formats[i] == GL_COMPRESSED_RGBA8_ETC2_EAC)
            supportNum++;
    }
    delete [] formats;

    return supportNum >= 2;
}
  • Configuration::gatherGPUInfo添加代码:
	supportsETC2 = checkForEtc2();
	_valueDict["gl.supports_ETC2"] = Value(_supportsETC2);
  • 加一个访问函数:Configuration::supportsETC2,和ETC1的流程一样,参考一下就懂了。

修改CCImage,支持ETC2的加载

  • 提供解析PKM2的函数如下:
// etc2
namespace
{
#define ETC2_RGB_NO_MIPMAPS           1
#define ETC2_RGBA_NO_MIPMAPS          3

    static const int ETC2_PKM_HEADER_SIZE = 16;
    static const char ETC2_PKM_MAGIC[] = { 'P', 'K', 'M', ' ', '2', '0' };

    static const uint32_t ETC2_PKM_FORMAT_OFFSET = 6;
    static const uint32_t ETC2_PKM_ENCODED_WIDTH_OFFSET = 8;
    static const uint32_t ETC2_PKM_ENCODED_HEIGHT_OFFSET = 10;
    static const uint32_t ETC2_PKM_WIDTH_OFFSET = 12;
    static const uint32_t ETC2_PKM_HEIGHT_OFFSET = 14;

    static uint32_t read_big_endian_uint16(const uint8_t *pIn) {
        return (pIn[0] << 8) | pIn[1];
    }

    static bool etc2_pkm_is_valid(const uint8_t* pHeader) {
        if (memcmp(pHeader, ETC2_PKM_MAGIC, sizeof(ETC2_PKM_MAGIC))) {
            return false;
        }
        uint32_t format = read_big_endian_uint16(pHeader + ETC2_PKM_FORMAT_OFFSET);
        uint32_t encodedWidth = read_big_endian_uint16(pHeader + ETC2_PKM_ENCODED_WIDTH_OFFSET);
        uint32_t encodedHeight = read_big_endian_uint16(pHeader + ETC2_PKM_ENCODED_HEIGHT_OFFSET);
        uint32_t width = read_big_endian_uint16(pHeader + ETC2_PKM_WIDTH_OFFSET);
        uint32_t height = read_big_endian_uint16(pHeader + ETC2_PKM_HEIGHT_OFFSET);
        return (format == ETC2_RGB_NO_MIPMAPS || format == ETC2_RGBA_NO_MIPMAPS) &&
               encodedWidth >= width && encodedWidth - width < 4 &&
               encodedHeight >= height && encodedHeight - height < 4;
    }

    static uint32_t etc2_pkm_get_width(const uint8_t * pHeader) {
        return read_big_endian_uint16(pHeader + ETC2_PKM_WIDTH_OFFSET);
    }

    static uint32_t etc2_pkm_get_height(const uint8_t* pHeader){
        return read_big_endian_uint16(pHeader + ETC2_PKM_HEIGHT_OFFSET);
    }

    static uint32_t etc2_pkm_get_format(const uint8_t* pHeader) {
        return read_big_endian_uint16(pHeader + ETC2_PKM_FORMAT_OFFSET);
    }
}

有了上面的说明,相信这个代码很容易看懂的。

  • Image::Format增加一种类型: ETC2

  • Image::detectFormat增加如下代码:

    else if (isEtc2(data, dataLen))
    {
        return Format::ETC2;
    }
  • 实现isEtc2:
bool Image::isEtc2(const unsigned char *data, ssize_t dataLen)
{
    return dataLen >= ETC2_PKM_HEADER_SIZE && etc2_pkm_is_valid(data);
}
  • 修改Image::initWithImageData:
	case Format::ETC2:
		ret = initWithETC2Data(unpackedData, unpackedLen);
		break;
  • 实现initWithETC2Data:
bool Image::initWithETC2Data(const unsigned char * data, ssize_t dataLen)
{
    const unsigned char* header = data;

    //check the data
    if (!etc2_pkm_is_valid(header))
    {
        return  false;
    }

    _width = etc2_pkm_get_width(header);
    _height = etc2_pkm_get_height(header);

    if (0 == _width || 0 == _height)
    {
        return false;
    }

    if (Configuration::getInstance()->supportsETC2())
    {
        uint32_t format = etc2_pkm_get_format(header);
        if (format == ETC2_RGB_NO_MIPMAPS)
            _renderFormat = Texture2D::PixelFormat::ETC2_RGB;
        else
            _renderFormat = Texture2D::PixelFormat::ETC2_RGBA;
        _dataLen = dataLen - ETC2_PKM_HEADER_SIZE;
        _data = static_cast<unsigned char*>(malloc(_dataLen * sizeof(unsigned char)));
        memcpy(_data, static_cast<const unsigned char*>(data) + ETC2_PKM_HEADER_SIZE, _dataLen);
        _hasPremultipliedAlpha = false;
        return true;
    }
    CCLOG("cocos2d: Hardware ETC2 decoder not support.");
    return false;
}

到此CCImage修改完毕。

修改CCTexture2D,生成ETC2纹理

  • Texture2D::PixelFormat增加两种类型:
	//! ETC2-compressed texture: GL_COMPRESSED_RGB8_ETC2
	ETC2_RGB,
	//! ETC2-compressed texture: GL_COMPRESSED_RGBA8_ETC2_EAC
	ETC2_RGBA,
  • static const PixelFormatInfoMapValue TexturePixelFormatInfoTablesValue[]增加这两种格式的信息:
	PixelFormatInfoMapValue(Texture2D::PixelFormat::ETC2_RGB, Texture2D::PixelFormatInfo(GL_COMPRESSED_RGB8_ETC2, 0xFFFFFFFF, 0xFFFFFFFF, 4, true, false)),
	PixelFormatInfoMapValue(Texture2D::PixelFormat::ETC2_RGBA, Texture2D::PixelFormatInfo(GL_COMPRESSED_RGBA8_ETC2_EAC, 0xFFFFFFFF, 0xFFFFFFFF, 8, true, true)),
  • Texture2D::getStringForFormat增加格式说明,其实不加也没关系:
	case Texture2D::PixelFormat::ETC2_RGB:
		return "ETC2_RGB";
	case Texture2D::PixelFormat::ETC2_RGBA:
		return "ETC2_RGBA";
  • Texture2D::initWithMipmaps判断压缩纹理处作一点修改:
	if (info.compressed && !Configuration::getInstance()->supportsPVRTC()
                        && !Configuration::getInstance()->supportsETC()
                        && !Configuration::getInstance()->supportsETC2())
    {
        CCLOG("cocos2d: WARNING: PVRTC/ETC/ETC2 images are not supported");
        return false;
    }

到此引擎改造完成。

Android版本,Android Studio的build.gradle的minSdkVersion改为18,即Android4.3,我没试过不改的话能不能正常使用,你们有兴趣可以试试看。

IOS版本,XCode直接编译,连上真机就可以测,不过要记得上面的EGLContext一定要换成3的,不然不会成功。

51赞

真是写得很棒很清晰的一篇教程!

什么时候把这个支持直接加到引擎中 :)

估计要 2.1 了……

1赞

赞一个,不过大环境下还是不现实,主要是安卓碎片化太严重,要是跟ios那样的迭代速度,开发者肯定会让官方升级到es3.0

Android 4.3以下直接放弃了?

可以先收集一些数据再决定。
如果4.3以下还有一定的比例,就还是用ETC1+Alpha;IOS可以考虑换成ETC2

给个大概时间呗,3个月?还是半年?

感谢楼主分享~

支持下好文

感谢楼主分享~~~~

1赞

都说是 2.1 了…… 可能是 半年 至 两年

秒懂,到那时候,应该所有设备都支持了 :}}}}

卧槽。。。。

感谢楼主分享~

谢谢分享:+1:

为什么不在1.7就加入,只要给用户一些参数,不需要考虑老机型的项目直接改成ETC2.0不是很好吗

mark好东西

1.7 / 1.8 的任务已经排满了

Error uploading compressed texture level: 0 . glError: 0x0501
报错呢,能给个联系方式吗,想咨询与喜爱