{"id":343,"date":"2014-09-21T13:27:01","date_gmt":"2014-09-21T11:27:01","guid":{"rendered":"http:\/\/www.noxis-studio.com\/blog\/?p=343"},"modified":"2019-03-12T01:11:51","modified_gmt":"2019-03-11T23:11:51","slug":"texture-compression-in-papy-stampy","status":"publish","type":"post","link":"http:\/\/www.noxis-studio.com\/blog\/texture-compression-in-papy-stampy\/","title":{"rendered":"Texture compression in Papy Stampy"},"content":{"rendered":"<h2>Texture compression ? What for ?<\/h2>\n<p>I recently observed that PapyStampy was fillrate limited on some Android Devices (e.g. on Asus Transformer). I therefore decided to implement Texture compression in order to improve the performances.<br \/>\nOn Android devices several compressions are available, depending on the devices.<\/p>\n<ul>\n<li>Ericsson Texture Compression (<em>ETC<\/em>)<\/li>\n<li>ATI texture compression (ATITC)<\/li>\n<li>PowerVR Texture Compression (PVRTC)<\/li>\n<li>S3\/DX Texture Compression (S3TC\/DXTC)<\/li>\n<\/ul>\n<p>I early decided to skip ETC texture compression as alpha channels are not supported. I also decided to encapsulate the compressed textures in DDS files. As a matter of fact DDS file format has several advantages:<\/p>\n<ul>\n<li>On the contrary of raw textures, it is possible to get the texture resolution and several other properties.<\/li>\n<li>It is possible to have several mipmap levels<\/li>\n<\/ul>\n<p>&nbsp;<\/p>\n<h2>How to compress the textures ?<\/h2>\n<p>I used several command-line softwares in order to compress the textures:<\/p>\n<ul>\n<li>The compressonator : an AMD tool that allows to make several texture compressions<\/li>\n<li>ImageMagick : a tool that allows to make several image editions<\/li>\n<li>PvrTexTool: a tool to compress textures in the PVR formats<\/li>\n<\/ul>\n<p>I created the following Python script in order to automate the texture compression in PapyStampy. This script creates DDS files with suffixed filenames telling the compression used (e.g mytexture.png.atitc.dds). When the application tries to load a texture, it looks for a compressed version of the same texture by appending the suffix to the texture filename.<\/p>\n<pre lang=\"python\" line=\"1\">import os\r\n\r\n# Path where the source textures are located\r\ng_sourceDirectory =   os.path.join(os.curdir , \"assets\", \"levelBuilder\", \"textures\")\r\ng_destDirectory =   os.path.join(os.curdir , \"test\")\r\n\r\n# List of supported extensions\r\ng_supportedExtensions = [\"png\", \"jpg\"]\r\n\r\ng_ImageMagikPath = \"D:\\\\Dev\\\\tools\\\\ImageMagick-6.8.9-7\\\\convert.exe\"\r\ng_CompressonatorPath = \"TheCompressonator.exe\"\r\ng_PvrTexToolPath = \"C:\\Imagination\\PowerVR\\GraphicsSDK\\PVRTexTool\\CLI\\Windows_x86_32\\PVRTexToolCLI.exe\"\r\n\r\ng_codec_ATIC = \"ATICompressor.dll\"\r\ng_codec_S3TC = \"dxtc.dll\"\r\n\r\ng_fourCC_S3TC = \"DXT3\"\r\ng_fourCC_ATIC = \"ATCA\" # \"ATI2N\"\r\ng_fourCC_PVRTC= \"PVRTC1_4\"\r\n\r\ndef FlipImage(filepathSrc, filepathDst):\r\n    cmd = \"%s \\\"%s\\\" -flip -alpha on -channel RGBA PNG32:\\\"%s\\\"\" %(g_ImageMagikPath, filepathSrc, filepathDst)\r\n    #print cmd\r\n    os.system(cmd)\r\n\r\ndef ConvertWithCompressonator(filepathSrc, filepathDst, fourCC, codec):\r\n    cmd = \"%s -convert \\\"%s\\\" \\\"%s\\\" -codec %s +fourCC %s\" %(g_CompressonatorPath, filepathSrc, filepathDst, codec, fourCC)\r\n    #print cmd\r\n    os.system(cmd)\r\n\r\ndef ConvertWithPvrTexTool(filepathSrc, filepathDst, format): #  -flip y\r\n    cmd = \"%s -f %s -i \\\"%s\\\" -q pvrtcbest  -o \\\"%s\\\"\" %(g_PvrTexToolPath, format, filepathSrc, filepathDst)\r\n    #print cmd\r\n    os.system(cmd)\r\n\r\ndef ProcessFileS3TC(filepath, filename, extension, flippedImagePath):\r\n    # Flip the image\r\n    if flippedImagePath == None:\r\n        flippedImagePath = os.path.join(g_destDirectory , \"tmp.%s\" %extension)\r\n        FlipImage(filepath, flippedImagePath)\r\n    # Encode\r\n    finalImage = os.path.join(g_destDirectory , \"%s.s3tc.dds\" %filename)\r\n    ConvertWithCompressonator(flippedImagePath, finalImage, g_fourCC_S3TC, g_codec_S3TC)\r\n\r\ndef ProcessFileATITC(filepath, filename, extension, flippedImagePath):\r\n    # Flip the image\r\n    if flippedImagePath == None:\r\n        flippedImagePath = os.path.join(g_destDirectory , \"tmp.%s\" %extension)\r\n        FlipImage(filepath, flippedImagePath)\r\n    # Encode\r\n    finalImage = os.path.join(g_destDirectory , \"%s.atitc.dds\" %filename)\r\n    ConvertWithCompressonator(flippedImagePath, finalImage, g_fourCC_ATIC, g_codec_ATIC)\r\n\r\ndef ProcessFilePVRTC(filepath, filename, extension):\r\n    # Encode\r\n    finalImage = os.path.join(g_destDirectory , \"%s.pvrtc.dds\" %filename)\r\n    ConvertWithPvrTexTool(filepath, finalImage, g_fourCC_PVRTC)\r\n\r\ndef ProcessFiles(dir, extension):\r\n    # Process all the files of the directory\r\n    for file in os.listdir(dir):\r\n        if file.lower().endswith(\".%s\" %extension):\r\n            filepath = os.path.join(dir, file)\r\n            # Compute the flipped image as it will be reused\r\n            flippedImagePath = os.path.join(g_destDirectory , \"tmp.%s\" %extension)\r\n            FlipImage(filepath, flippedImagePath)\r\n            # Process for S3TC\r\n            ProcessFileS3TC(filepath, file, extension, flippedImagePath)\r\n            # Process for ATITC\r\n            ProcessFileATITC(filepath, file, extension, flippedImagePath)\r\n            # Process PVR\r\n            ProcessFilePVRTC(filepath, file, extension)\r\n\r\nProcessFiles(g_sourceDirectory, \"png\")<\/pre>\n<p>This script was written for my configuration and will need to be updated depending on your OS and depending on the location of your binaries\/source textures\/&#8230;<\/p>\n<h2>How to load compressed textures in Android?<\/h2>\n<p>As explained earlier, the compressed textures are encapsulated in DDS file. <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/bb943991%28v=vs.85%29.aspx\" title=\"This link\">This link<\/a> gives some details on the format.<br \/>\nThe following sample code shows how a compressed texture can be loaded from the input stream of a DDS file.<\/p>\n<pre lang=\"java\" line=\"1\">\r\n\r\npublic class TextureCompressionLoadingSample {\r\n\r\n\t\/\/ DDS fourCC\r\n\tpublic static final int FOURCC_DXT1 = 0x31545844;\r\n\tpublic static final int FOURCC_DXT3 = 0x33545844;\r\n\tpublic static final int FOURCC_DXT5 = 0x35545844;\r\n\tpublic static final int FOURCC_ATI2 = 0x32495441;\r\n\tpublic static final int FOURCC_PTC4 = 0x34435450;\r\n\tpublic static final int FOURCC_ATCA = 0x41435441;\r\n\t\r\n\t\/\/ S3TC internal formats\r\n\t\/\/ http:\/\/www.khronos.org\/registry\/gles\/extensions\/NV\/NV_texture_compression_s3tc.txt\r\n\tpublic static final int GL_COMPRESSED_RGB_S3TC_DXT1_EXT   = 0x83F0;\r\n\tpublic static final int GL_COMPRESSED_RGBA_S3TC_DXT1_EXT  = 0x83F1;\r\n\tpublic static final int GL_COMPRESSED_RGBA_S3TC_DXT3_EXT  = 0x83F2;\r\n\tpublic static final int GL_COMPRESSED_RGBA_S3TC_DXT5_EXT  = 0x83F3;\r\n\r\n\t\/\/ ATITC internal formats\r\n\t\/\/ http:\/\/www.khronos.org\/registry\/gles\/extensions\/AMD\/AMD_compressed_ATC_texture.txt\r\n\tpublic static final int ATC_RGB_AMD                       = 0x8C92;\r\n\tpublic static final int ATC_RGBA_EXPLICIT_ALPHA_AMD       = 0x8C93;\r\n\tpublic static final int ATC_RGBA_INTERPOLATED_ALPHA_AMD   = 0x87EE;\r\n\t\r\n\t\/\/ PVRTC internal formats\r\n\t\/\/ http:\/\/www.khronos.org\/registry\/gles\/extensions\/IMG\/IMG_texture_compression_pvrtc.txt\r\n\tpublic static final int COMPRESSED_RGB_PVRTC_4BPPV1_IMG   = 0x8C00;\r\n\tpublic static final int COMPRESSED_RGB_PVRTC_2BPPV1_IMG   = 0x8C01;\r\n\tpublic static final int COMPRESSED_RGBA_PVRTC_4BPPV1_IMG  = 0x8C02;\r\n\tpublic static final int COMPRESSED_RGBA_PVRTC_2BPPV1_IMG  = 0x8C03;\r\n\t\r\n\r\n\t\/\/ Texture ID\r\n\tint m_textureID = -1;\r\n\t\/\/ Tmp buffer\r\n\tbyte m_tmpBuffer[] = new byte[124];\r\n\t\r\n\tpublic boolean createCompressedTextureFromDDS(GL10 gl, InputStream is)\r\n\t{\r\n\t\tboolean res = true;\r\n\t\ttry\r\n\t\t{\r\n\t\t\t\/\/ Read the file type\r\n\t\t\tif (is.read(m_tmpBuffer, 0, 4) != 4)\r\n\t\t\t{\r\n\t\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: DDS file too short for file type\");\r\n\t\t\t\tis.close();\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\/\/ Check file type\r\n\t\t\tif (m_tmpBuffer[0] != 'D'\r\n\t\t\t\t|| m_tmpBuffer[1] != 'D'\r\n\t\t\t\t|| m_tmpBuffer[2] != 'S'\r\n\t\t\t\t|| m_tmpBuffer[3] != ' ')\r\n\t\t\t{\r\n\t\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: Wrong DDS file type\");\r\n\t\t\t\tis.close();\r\n\t\t\t\treturn false;\t\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\/\/ Read the surface desc\r\n\t\t\tif (is.read(m_tmpBuffer, 0, 124) != 124)\r\n\t\t\t{\r\n\t\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: DDS file too short for surface desc\");\r\n\t\t\t\tis.close();\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t\tByteBuffer wrapped = ByteBuffer.wrap(m_tmpBuffer); \/\/ big-endian by default \r\n\t\t\twrapped.order(ByteOrder.LITTLE_ENDIAN);\r\n\t\t\tint height      = wrapped.getInt(8 );\r\n\t\t\tint width       = wrapped.getInt(12 );\r\n\t\t\tint linearSize  = wrapped.getInt(16 );\r\n\t\t\tint mipMapCount = wrapped.getInt(24 );\r\n\t\t\tint fourCC      = wrapped.getInt(80 );\r\n\t\t\t\r\n\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: DDS file width=\" + width + \" height=\" + height+ \" fourCC=\" + fourCC );\r\n\r\n\t\t\t\r\n\t\t\t\/\/ Guess the internal format thanks to the FourCC\r\n\t\t\tint components  = (fourCC == FOURCC_DXT1) ? 3 : 4;\r\n\t\t\tint format;\r\n\t\t\tswitch(fourCC)\r\n\t\t\t{\r\n\t\t\tcase FOURCC_DXT1:\r\n\t\t\t\tformat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;\r\n\t\t\t\tbreak;\r\n\t\t\tcase FOURCC_DXT3:\r\n\t\t\t\tformat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;\r\n\t\t\t\tbreak;\r\n\t\t\tcase FOURCC_DXT5:\r\n\t\t\t\tformat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;\r\n\t\t\t\tbreak;\r\n\t\t\tcase FOURCC_ATCA:\r\n\t\t\t\tformat = ATC_RGBA_EXPLICIT_ALPHA_AMD;\r\n\t\t\t\tbreak;\r\n\t\t\t\t\r\n\t\t\tcase FOURCC_PTC4:\r\n\t\t\t\tformat = COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;\r\n\t\t\t\tbreak;\r\n\t\t\t\t\r\n\t\t\tdefault:\r\n\t\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: Unsupported FourCC type\");\r\n\t\t\t\tis.close();\r\n\t\t\t\treturn false;\t\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\r\n\t\t\t\/\/ create the new texture\r\n\t\t\tint[] tex_out = new int[1];\r\n\t\t\tgl.glGenTextures(1, tex_out, 0);\r\n\r\n\t\t\tm_textureID = tex_out[0];\r\n\t\t\tgl.glBindTexture(GL10.GL_TEXTURE_2D, m_textureID);\r\n\r\n\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, m_textureWrapS);\r\n\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, m_textureWrapT);\r\n\t\t\tgl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);\r\n\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tif (m_textureFiltering == TextureFiltering.TextureFilteringNearest)\r\n\t\t\t\t{\r\n\t\t\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); \r\n\t\t\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_NEAREST);\r\n\t\t\t\t\tglTexImage2D(GL10.GL_TEXTURE_2D, 0, m_bitmap, 0, gl);\r\n\t\t\t\t}\r\n\t\t\t\telse if (m_textureFiltering == TextureFiltering.TextureFilteringLinear)\r\n\t\t\t\t{\r\n\t\t\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); \r\n\t\t\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);\r\n\t\t\t\t}\r\n\t\t\t\telse if (m_textureFiltering == TextureFiltering.TextureFilteringMipMap)\r\n\t\t\t\t{\r\n\t\t\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR_MIPMAP_LINEAR); \/\/ GL_LINEAR_MIPMAP_NEAREST\r\n\t\t\t\t\tgl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);\r\n\t\t\t\t \t\t\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tcatch(Exception e)\r\n\t\t\t{\r\n\t\t\t\tLog.e(\"LoadBitmap\", \"Texture Load Error: \", e);\r\n\t\t\t\t\r\n\t\t\t\tm_textureID = -1;\r\n\t\t\t\tUnloadBitmap();\r\n\t\t\t\t\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t\/\/ The block-size is 8 bytes for DXT1, BC1, and BC4 formats, and 16 bytes for \r\n\t\t\t\/\/ other block-compressed formats.\r\n\t\t\t\/\/ ATI1N (BC4\/DXT5A), ATI2N (BC5\/3Dc)\r\n\t\t\t\/\/ http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/bb943991%28v=vs.85%29.aspx\r\n\t\t\tint blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;\r\n\t\t\tint offset = 0;\r\n\t\t \r\n\t\t\t\/* load the mipmaps *\/\r\n\t\t\tfor (int level = 0; level < mipMapCount &#038;&#038; width > 0 && height > 0; ++level)\r\n\t\t\t{\r\n\t\t\t\t\/\/ COmpute the size of the mimmap level texture\r\n\t\t\t\t\/\/ http:\/\/msdn.microsoft.com\/en-us\/library\/windows\/desktop\/bb943991%28v=vs.85%29.aspx\r\n\t\t\t\tint size = ((width+3)\/4)*((height+3)\/4)*blockSize;\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Can be optimized by making a single allocation\r\n\t\t\t\tbyte tmpBuffer2[] = null;\r\n\t\t\t\ttmpBuffer2 = new byte[size];\r\n\t\t\t\tif (is.read(tmpBuffer2, 0, size) != size)\r\n\t\t\t\t{\r\n\t\t\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: DDS file too short for texture data\");\r\n\t\t\t\t\tis.close();\r\n\t\t\t\t\treturn false;\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\t\/\/ Create the texture level\r\n\t\t\t\tgl.glCompressedTexImage2D(GL11.GL_TEXTURE_2D, level, format, width, height, \r\n\t\t\t\t\t\t\t\t\t\t\t0, size, ByteBuffer.wrap( tmpBuffer2));\r\n\t\t \r\n\t\t\t\toffset += size;\r\n\t\t\t\twidth  \/= 2;\r\n\t\t\t\theight \/= 2;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tis.close();\r\n\t\t\t\r\n\t\t\tLog.i(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION Success !!!\");\r\n\r\n\t\t\treturn true;\r\n\t\t}\r\n\t\tcatch (Exception e)\r\n\t\t{\r\n\t\t\tLog.e(\"TEXTURECOMPRESSION\", \"TEXTURECOMPRESSION: Failed to parse DDS file\", e);\r\n\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n\r\n}\r\n\r\n<\/pre>\n<p>The internal formats of each compressed format can be found on the khronos website. The fourCC can be simply found by editing your DDS files with an Hexadecimal editor.<\/p>\n<p>&nbsp;<\/p>\n<h2>Impact on performances<\/h2>\n<p>I measured the impact on the performances at a location where the framerate used to drop and I obtained the following results.<\/p>\n<table border=\"1\">\n<tbody>\n<tr>\n<td><\/td>\n<td colspan=\"3\">Transformer<\/td>\n<td colspan=\"3\">Xperia L<\/td>\n<td colspan=\"3\">OUYA<\/td>\n<\/tr>\n<tr>\n<td><\/td>\n<td>High RGB32<\/td>\n<td>Medium DTX<\/td>\n<td>Low RGB 565<\/td>\n<td>High RGB32<\/td>\n<td>Medium ATI<\/td>\n<td>Low RGB 565<\/td>\n<td>High RGB32<\/td>\n<td>Medium DTX<\/td>\n<td>Low RGB 565<\/td>\n<\/tr>\n<tr>\n<td>Framerate<\/td>\n<td>20,5<\/td>\n<td>29,8<\/td>\n<td>34<\/td>\n<td>40,5<\/td>\n<td>46<\/td>\n<td>44<\/td>\n<td>34<\/td>\n<td>40<\/td>\n<td>50<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>These results show that texture compression clearly improve the performances. We can also see that loading textures without compression but in RGB565 gives the best framerates.<\/p>\n<h2>Conclusion<\/h2>\n<p>As a conclusion, Texture compression improves the performances but can have an impact on the texture quality. It is the same with the alternative of using textures in RGB565 format.<br \/>\nAs a result, you should select carefully which textures should be compressed and which textures should remain in RGB32.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Texture compression ? What for ? I recently observed that PapyStampy was fillrate limited on some Android Devices (e.g. on Asus Transformer). I therefore [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-343","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"acf":{"full_width":false,"header_transparent":false},"aioseo_notices":[],"_links":{"self":[{"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/posts\/343","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/comments?post=343"}],"version-history":[{"count":7,"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/posts\/343\/revisions"}],"predecessor-version":[{"id":351,"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/posts\/343\/revisions\/351"}],"wp:attachment":[{"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/media?parent=343"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/categories?post=343"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.noxis-studio.com\/blog\/wp-json\/wp\/v2\/tags?post=343"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}