Android NDK中ETC1纹理的加载和Alpha通道处理

发表于2015-10-06
评论0 5.2k浏览

Android NDK中ETC1纹理的加载和Alpha通道处理

本文将论述如何使用ETC1纹理。

压缩纹理有什么好处?

你可能对诸如.png和.jpg的图片很熟悉。.png采用的是无损压缩的方式,而.jpg采用的是有损压缩。通常情况下,对同一图像采用JPG格式时,降低质量的代价更小一些。
不论图像在磁盘上采用什么方式存储,当把它加载到内存中时,图像都会解压到原始大小。对于RGBA 8888,每个像素占32bits,所以,当你加载1024*1024大小的纹理时,可能会使用掉4M的宝贵内存。
抛开这些知名的图像格式不说,也有一些格式会对纹理进行压缩,当将其加载到GPU上时,图像仍然是压缩的。这些格式能够最大限度的降低每个像素需要的比特位。
然而,当在Android上编码时,这些格式都是和供应商有特定联系的。目前,主流的有四个:ETC1,PVRTC,ATITC,S3TC。可以在http://developer.android.com/guide/topics/graphics/opengl.html#textures查看更多格式。
因为在Android设备上只有ETC1可用,所以本文将只讨论这种格式。对于ETC1,有个很大的问题是:ETC1并不支持带有alpha通道的纹理。幸运的是有几种方法可以绕过这个问题。就像之前提到的,这些压缩格式意味着纹理在GPU上仍然保持着压缩状态,而会在运行时进行解压,这将在降低纹理占用空间的同事,增加openGL游戏的性能(通过减少数据带宽实现)。
另一个使用这些纹理格式的好处是:你不需要将任何未打包的图片导入到你的游戏中;你也不需要在java侧加载图片,正如我在更早的一篇文章中描述的那样。(http://sbcgamesdev.blogspot.com/2012/12/load-images-under-android-with-ndk-and.html
使用ETC1D的一个缺点是:在压缩格式下的纹理比有同样图片的.jpg或.png文件大,因此需要占用更多的硬盘空间。以后我将在文章中说明我是怎么处理这个问题的。

创建ETC1纹理

每个GPU供应商都有自己的一套工具集,包括用于纹理压缩的工具。我使用的是来自ARM的工具(http://malideveloper.arm.com/develop-for-mali/mali-gpu-texture-compression-tool/)。运行该工具后,你将能看到初始化界面。打开纹理对应的图片(千万不要忘记open GL ES需要高和宽都是2的幂次方),你将能看到如下界面:

在左侧面板上选择对应的纹理,并点击“压缩”图标。这时,将会弹出压缩参数面板,如下所示:

然后,选择ETC1/ECT2页,并选择PKM作为输出格式。PKM是一种非常简单地格式,该格式只是在压缩数据上加一个很小的头部。头部如下所示:
- 0:4bytes的头“PKM” - 4:2bytes的版本“10” - 6:2bytes数据类型(总是为0) - 8:2bytes延生宽度 - 10:2bytes延生高度 - 12:2bytes原始宽度 - 14:2bytes原始高度 - 16:压缩的纹理数据
在ETC1中,每个44的像素块会被压缩成64bits。因此扩展宽度和高度是由原始尺寸的四的倍数,并进行四舍五入后得到。如果你用的是2的幂次方的纹理,则原始和扩展尺寸是一样的。
通过这些参数,你能够这样计算压缩数据的大小:
(扩展宽度/4)
(扩展高度/4)8 这个公式说明了有如此多的44个像素块,每个像素块有8个bytes长(64bits)。
上图中标记为2和5的参数将影响压缩质量。压缩过程会耗费一些时间。因此在开发过程中,如果你不想等待,可以使用差一点的质量。但不要忘了在游戏开发结束时使用最大化的质量---使输出的大小保持一致。
如标记3所示,不要忘了选择ETC1,如标记4所示,不要忘了选择“create separate texture for alpha channel”。这里的纹理将原始的有相同的尺寸。但是该纹理的red通道将被用于存储alpha通道而不是颜色。green和blue通道没有被使用,所以理论上开发者可以放任意额外数据在上面(但不能使用这里介绍的工具--这取决于你怎么使用它)。

加载ETC1纹理

现在,已经有了压缩好的纹理了。该是把它加载到GPU的时间了。

//------------------------------------------------------------------------u16 TextureETC1::swapBytes(u16 aData){ return ((aData & 0x00FF) << 8) | ((aData & 0xFF00) >> 8);}//------------------------------------------------------------------------void TextureETC1::construct(SBC::System::Collections::ByteBuffer& unpacked){ // check if data is ETC1 PKM file - should start with text "PKM " (notice the space in the end) // read byte by byte to prevent endianness problems u8 header[4]; header[0] = (u8) unpacked.getChar(); header[1] = (u8) unpacked.getChar(); header[2] = (u8) unpacked.getChar(); header[3] = (u8) unpacked.getChar(); if (header[0] != 'P' || header[1] != 'K' || header[2] != 'M' || header[3] != ' ') LOGE("data are not in valid PKM format");

swapBytes是一个帮助方法,真正的工作在构造方法中实现。ByteBuffer是一个简单的包装器,里面不仅有数据,还有大小。并不是一定要这样实现,这里的实现方式仅仅是为了增加可读性。
以下将检查输入数据是否是真的PKM文件。

// read version - 2 bytes. Should be "10". Just skipping unpacked.getShort(); // data type - always zero. Just skip unpacked.getShort(); // sizes of texture follows: 4 shorts in big endian order u16 extWidth = swapBytes((u16) unpacked.getShort()); u16 extHeight = swapBytes((u16) unpacked.getShort()); u16 width = swapBytes((u16) unpacked.getShort()); u16 height = swapBytes((u16) unpacked.getShort()); // calculate size of data with formula (extWidth / 4) * (extHeight / 4) * 8 u32 dataLength = ((extWidth >> 2) * (extHeight >> 2)) << 3;

下一步,因为我们用不上,所以跳过增加版本信息和数据类型。然后,读取尺寸信息,并通过swapByte将尺寸信息从big字节转换到little字节。压缩后的数据大小是通过上文中提到的公式计算得到的。

// openGL part // create and bind texture - all next texture ops will be related to it glGenTextures(1, &mTextureID); glBindTexture(GL_TEXTURE_2D, mTextureID); // load compressed data (skip 16 bytes of header) glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_ETC1_RGB8_OES,extWidth, extHeight, 0, dataLength, unpacked.getPositionPtr()); // set texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // save size mWidth = extWidth; mHeight = extHeight;}

下一步到了创建OpenGL方法创建纹理的时候了。首先我们创建纹理ID,然后给他设置数据。需要注意的是内部格式是GLETC1RGB8OES。这就是说我们使用OEScompressedETC1RGB8_texture扩展增加对ETC1纹理的支持。

在ETC1纹理中处理alpha通道

正如之前说过的,ETC1不支持alpha通道。在纹理创建过程中,我们将alpha通道数据导出到单独的纹理中。这个纹理和带有颜色的纹理有相同的尺寸。最终,可以通过使用片段着色器从这两张纹理中形成最终的包含alpha通道的颜色纹理。
这个简单的片段着色器如下所示:

#ifdef GL_ESprecision mediump float;#endifuniform lowp sampler2D u_map[2];varying mediump vec2 v_texture;void main(void){ gl_FragColor = vec4(texture2D(u_map[0], v_texture).rgb, texture2D(u_map[1], v_texture).r);}

在将我自己的游戏中之前使用.png文件的纹理改成使用支持带有alpha通道的ETC1纹理,花费了我不到一个小时时间。最终,在三星Galaxy Tab上测试,性能得到了10%的提升。

压缩成2的幂次方

最后一个让人不愉快的是不带alpha通道的ETC1纹理是.jpg文件的2倍(在我的之前的游戏http://sbcgamesdev.blogspot.com/2013/03/shards-work-in-progress-i.html中,使用.jpg作为背景--所以不需要alpha通道)。最终,我使用http://zlib.net/库对.pkm文件进行额外的压缩解决了这个问题。为了找到如何在Android上使用它,花费了我很多时间。但是你可以从http://sbcgamesdev.blogspot.com/2013/06/using-zlib-compression-library-in.html找到如何使用。
现在,在我的游戏中,压缩后的大小几乎和.jpg文件一样大。当创建纹理时,先用zlib进行解压,在解压.pkm文件。

结论

ETC1纹理压缩是唯一的Android设备所支持的(当然有OpenGL ES2.0)。但是ETC1缺少alpha通道,所以为了绕过这个问题需要一点点开销。在我的三星Galaxy Tab设备上,使用ETC1保存纹理时能够增加10%的帧速率。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引