關於cocos2d-x對etc1圖片支持的分析

 http://blog.csdn.net/langresser_king/article/details/9339313 
         
         1、ETC1圖片是android下通用的壓縮紋理,幾乎所有的android機器都支持,是opengles2.0的標準。不像pvrtc4只是部分powervr的顯卡支持。 
         ETC1圖片不支持半透明(有替代方案可以使etc1圖片兼容半透明顯示),內存佔用只有正常RGBA8888的八分之一(一個像素0.5個字節),並且具備極高的加載速度。ETC1的圖片大小隻跟圖片尺寸相關,在大小上無法媲美jpg或者png8的圖片。 
         2、cocos2d-x早期使用android提供的ETC1Util來加載紋理,後面經過一次優化,改變成直接讀取文件的加載方式。 也就是說ETC1文件前面16個字節是文件頭,包含文件寬高等信息。 除開這16個字節,剩下的就是圖片像素數據,這些數據可以直接傳遞給顯卡使用glCompressedTexImage2D來創建紋理。 
         3、同樣在這次優化中,加入了軟件解壓ETC1的功能,這樣windows等桌面平臺也可以使用ETC1的圖片了(雖然沒有任何優勢可言)。但是實現有一些bug,導致不兼容非2的整次冪的圖片。修改如下 
          
         
          
           
           [cpp] 
           view plain
           copy 
            
            
           
          
          
           //if it is not gles or device do not support ETC, decode texture by software 
            int bytePerPixel = 3; 
            GLenum fallBackType = GL_UNSIGNED_BYTE; 
            
            /*bool fallBackUseShort = false; 
            if(fallBackUseShort) 
            { 
            bytePerPixel = 2; 
            fallBackType = GL_UNSIGNED_SHORT_5_6_5; 
            } 
            */ 
            unsigned int stride = _width * bytePerPixel; 
            
            std::vector<unsigned char> decodeImageData(((stride + 3) &~ 3) * ((_height + 3) &~3)); 
            
            etc1_decode_image(etcFileData + ETC_PKM_HEADER_SIZE, &decodeImageData[0], _width, _height, bytePerPixel, ((stride + 3) &~ 3)); 
            
            //set decoded data to gl 
            glGenTextures(1, &_name); 
            glBindTexture(GL_TEXTURE_2D, _name); 
            
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_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); 
            
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, _width, _height, 0, GL_RGB, fallBackType, &decodeImageData[0]); 
            
            glBindTexture(GL_TEXTURE_2D, 0); 
            delete[] etcFileData; 
            etcFileData = NULL; 
            return true; 
          
         
         
        注意其中兩句  
          
         std::vector<unsigned char> decodeImageData(((stride + 3) &~ 3) * ((_height + 3) &~3)); 
         etc1_decode_image(etcFileData + ETC_PKM_HEADER_SIZE, &decodeImageData[0], _width, _height, bytePerPixel, ((stride + 3) &~ 3)); 
         分配內存必須能夠容納下圖片數據,而ETC1圖片會進行4字節對齊(圓整),所以寬高不能直接使用原始圖片數據。 當然,不修改的話對於2的整次冪的圖片也是沒有問題的,因爲本身就是對齊的,不需要圓整了。 
           
         4、android下部分機器兼容非2的整次冪的etc1圖片,但是同樣也有部分機器不兼容。遇到非2的整次冪的圖片會渲染錯誤甚至崩潰。所以android下使用etc1圖片需要進行2的整次冪的擴展。如果大量零碎文件的話,考慮使用TexturePacker打包圖片 
           
         5、etc1對透明圖片的支持。 etc1不支持透明圖片,同樣cocos2d-x對etc1也不支持透明圖片的顯示。雖然圖片格式上面不支持,但是我們可以通過技術手段間接達到透明etc1圖片渲染的目的。詳細內容可以參考這裏 。 
         有兩種方案可以選擇,一種是通過Mali工具生成pkm文件時選擇Create atlas,這樣就生成了一張拼接在一起的紋理。這張紋理上半部分是原始圖片(無alpha信息),下半部分是alpha信息圖片。在渲染的時候使用特殊的shader進行渲染。這個改動是比較小的。 
         另一種方案是創建兩張分離的圖片,分別是原始圖片和alpha圖片。渲染時加載這兩張紋理,然後alpha圖片當做參數傳遞給原始圖片的shader。 
         我使用的是第一種方案。修改後的shader如下(注意,這個shader是新增的,並且是隻有這種打包的etc1圖片才使用這個shader,未打包的無透明色的etc1圖片和png圖片依然使用原來的shader) 只需要修改像素着色器代碼,頂點着色器代碼不變。 由於現在etc支持透明顯示了,所以bool CCTexture2D::initWithETCFile(const char* file)中m_bHasPremultipliedAlpha要置爲false,開啓alpha blend來渲染圖片 
          
         
          
           
           [cpp] 
           view plain
           copy 
            
            
           
          
          
            
           #ifdef GL_ES  
           precision lowp float; 
           #endif  
            
           varying vec4 v_fragmentColor; 
           varying vec2 v_texCoord; 
           uniform sampler2D CC_Texture0; 
           void main() 
           { 
            gl_FragColor = vec4(texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y)).xyz, texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y + 0.5)).r); 
           } 
          
         
         
         
          
         6、使用etc1圖片可以極大的減少內存,並且加快加載速度。 我做過一個簡單的測試,80k的png8的圖片加載需要消耗117ms,同樣的etc1圖片(經過擴展有1mb大小)加載消耗40ms。這個已經是極限情況。 一般來說同樣大小的etc1圖片加載速度要快5~10倍。 
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~下面新的研究成果~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
         7、關於PremultipliedAlpha的理解。 cocos2d-x的CCTexture2D中有一個m_bHasPremultipliedAlpha的屬性。我們使用TexturePacker中導出pvr圖片時也有提示開啓PVRImagesHavePremultipliedAlpha這個選項。 雖然PremultipliedAlpha就是圖片的顏色在輸出的時候已經預先乘以alpha色了,所以渲染的時候圖片的RGB就需要再次乘以alpha色了,這個在一定程度上可以提高運行效率。所以TexturePacker推薦開啓PremultipliedAlpha選項,XCode導出png圖片的時候以及UIImage加載圖片的時候都會使用PremultipliedAlpha。 這個有一點噁心的地方就是,我們無法通過一個圖片屬性判斷它是否是是PremultipliedAlpha的,只能通過肉眼或者是一個並不準確的公式來判斷。 
         我們還可以進一步去理解這個設置。一般來說,半透明圖片渲染使用的是alpha blend 參見CCSprite::updateBlendFunc()這個函數。 
          
         
          
           
           [cpp] 
           view plain
           copy 
            
            
           
          
          
           void CCSprite::updateBlendFunc(void) 
           { 
            CCAssert (! m_pobBatchNode, "CCSprite: updateBlendFunc doesn't work when the sprite is rendered using a CCSpriteBatchNode"); 
            
            // it is possible to have an untextured sprite 
            if (! m_pobTexture || ! m_pobTexture->hasPremultipliedAlpha()) 
            { 
            m_sBlendFunc.src = GL_SRC_ALPHA; 
            m_sBlendFunc.dst = GL_ONE_MINUS_SRC_ALPHA; 
            setOpacityModifyRGB(false); 
            } 
            else 
            { 
            m_sBlendFunc.src = CC_BLEND_SRC; 
            m_sBlendFunc.dst = CC_BLEND_DST; 
            setOpacityModifyRGB(true); 
            } 
           } 
          
         
         
        正常來說,半透明圖片渲染使用的是 GL_SRC_ALPHA GL_ON_MINUS_SRC_ALPHA這個選項 代表的意思就是: 源(圖片)像素*源因子(源alpha) + 目標(屏幕)像素*目標因子(1-源alpha)。 通過這個公式可以達到渲染半透明圖片的目的。 
          
         如果圖片有PremultipliedAlpha,再使用這個公式就不對了,圖片明顯變暗,因爲圖片的rgb已經乘以alpha了,再乘一次圖片自然就變黑一點。 這個時候渲染的公式就變爲: 
         源像素 + 目標像素*(1-源alpha)。 雖然圖片依然是半透明的,但是處理源像素時不再分別乘alpha了。 
           
         8、爲什麼要特意提這個屬性呢? 因爲ETC1圖片在加載的時候默認開啓了PremultipliedAlpha,一般不透明的圖片處理起來沒有問題(正常的etc1圖片就是不透明的),但是參見上面我們的透明etc1圖片渲染解決方案,實際圖片在渲染的時候是可以達到半透明的效果的。所以我們有兩個選擇,一個是默認關閉PremultipliedAlpha,另一個是默認開啓PremultipliedAlpha然後shader中分別把rgb乘以alpha。 具體是alpha blend效率高還是shader中效率高我還沒有測試。 
           
         9、使用上面的shader在渲染的時候windows下正常,但是android下會出現大量的鋸齒。一開始以爲是mipmap沒有開啓的緣故,但是使用mipmap(後面會介紹)後,依然無法解決問題。後面發現cocos2d-x中shader默認使用的低精度浮點數  
          
         
          
           
           [cpp] 
           view plain
           copy 
            
            
           
          
          
           #ifdef GL_ES \n\ 
           precision lowp float; \n\ 
           #endif \n\ 
          
         
         
        低精度浮點數有效位數因顯卡而異,但是不高是肯定的。如果我們沒有特殊的運算,低精度足夠使用。但是一旦我們有*0.5之類的運算,那麼低精度浮點數很容易丟失數據,那表現出來就是各種鋸齒。 所以在新的shader代碼中刪除了這個指令。 另外某些文檔說,使用低精度無助於效率提升,因爲最終渲染的時候還是要轉回中精度(中精度是默認選項,部分高級顯卡支持高精度) 
          
           
         10、最終修改後的shader如下 
         頂點shader (我們把部分運算移動到頂點shader中,而不是每個像素進行計算,這個可以提升運行效率) 
          
         
          
           
           [cpp] 
           view plain
           copy 
            
            
           
          
          
            
           attribute vec4 a_position; 
           attribute vec2 a_texCoord; 
           attribute vec4 a_color; 
            
           varying vec4 v_fragmentColor; 
           varying vec2 v_texCoord; 
           varying vec2 v_alphaCoord; 
            
           void main() 
           { 
            gl_Position = CC_MVPMatrix * a_position; 
            v_fragmentColor = a_color; 
            v_texCoord = a_texCoord * vec2(1.0, 1.0); 
            v_alphaCoord = v_texCoord + vec2(0.0, 0.5); 
           } 
          
         
         
        像素shader 
          
         
          
           
           [cpp] 
           view plain
           copy 
            
            
           
          
          
           varying vec4 v_fragmentColor; 
           varying vec2 v_texCoord; 
           varying vec2 v_alphaCoord; 
           uniform sampler2D CC_Texture0; 
           void main() 
           { 
            vec4 v4Colour = texture2D(CC_Texture0, v_texCoord); 
            v4Colour.a = texture2D(CC_Texture0, v_alphaCoord).r; 
            v4Colour.xyz = v4Colour.xyz * v4Colour.a; 
            gl_FragColor = v4Colour * v_fragmentColor; 
            
            //gl_FragColor = vec4(texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y)).xyz, texture2D(CC_Texture0, vec2(v_texCoord.x, v_texCoord.y + 0.5)).r); 
           } 
          
         
         
        關於shader需要說明三點,在頂點shader中有這麼一條指令 v_texCoord = a_texCoord * vec2(1.0, 1.0); 因爲ETC1需要2的整次冪,所以我們的圖片基本上都有擴展,那也就意味着會設置setTextureRect,如果設置了這個,那麼a_texCoord就是我們指定的大小,所以這裏去的是(1.0, 1.0),如果沒有setTextureRect,那麼a_texCoord就是全部的貼圖大小,也就是兩倍的正常大小,那麼這個時候取的就應該是(1.0, 0.5)。 最終我的解決方法是所有的使用這個shader的圖片都設置一下大小。這樣shader就統一了。 
          
         在像素着色器代碼中有v4Colour.xyz = v4Colour.xyz * v4Colour.a; 這個就跟上面說的PremultipliedAlpha有關係。我們在shader中預先乘以alpha。 
         這個shader的使用條件,只有帶透明的etc1圖片(通過工具導出時進行了自動拼接)才能使用這個shader進行渲染,否則都會出錯。這個我們要在代碼中進行判斷。 
          
        
          11、關於圖片拼接時的黑邊問題。
         
        
          這個可以單獨開一個話題,但是由於是我處理ETC1圖片時遇到的,所以統一都在這裏解析了。 網上經常看到有人說圖片拼接的時候有黑邊,比如tilemap地圖拼接的時候。這個分三種情況,一種是最簡單的圖片對齊計算有問題,拼接的時候由於浮點數計算多了一個像素或者是少了一個像素,這個計算的時候有意向做移動一個像素就可以解決。
         
        
          第二種是圖片導出的問題(使用TexturePacker),不僅僅是地圖拼接黑邊,可能其他資源也會有黑的虛線,這個在導出的時候選擇--border 2 --shape 2 (TexturePacker中有對應的設置,默認爲0,但是我之前手欠給修改成了0)。 另外還有一個Exclude選項也是用來解決這個問題的。
         
        
          第三種是最本質的問題,比如我碰到的使用png圖片渲染正常,但是使用etc1圖片渲染就出現黑邊,若隱若現,一拖動界面就出現。 這個可以在紋理創建的時候設置這個來解決(png等圖片創建的時候有設置,但是etc1沒有)
         
         
          
           
            
            [cpp] 
            view plain
            copy 
             
             
            
           
           
            glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 
             glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 
           
          
          一般來說,紋理通過
         
         
          
           
            
            [cpp] 
            view plain
            copy 
             
             
            
           
           
            if (isMipmapped) { 
             /* Enable bilinear mipmapping */ 
             glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); 
             } else { 
             glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 
             } 
           
          
          這個來進行抗鋸齒等操作,但是如果在圖片邊緣的時候計算就會有問題,因爲外部沒有像素了,而圖片本身像素爲半透明,那麼計算的時候很有可能計算出黑色,那麼就顯示出黑邊了。
         
         
         
        
          12、最後要說下mipmap,mipmap就是圖片如果有縮小,那麼渲染的時候使用小的圖片(比如256*256的圖片如果縮小一半來渲染,就取128*128的圖片),這個小的圖片可以直接使用函數生成
         
         
          
           
            
            [cpp] 
            view plain
            copy 
             
             
            
           
           
            void CCTexture2D::generateMipmap() 
            { 
             CCAssert( m_uPixelsWide == ccNextPOT(m_uPixelsWide) && m_uPixelsHigh == ccNextPOT(m_uPixelsHigh), "Mipmap texture only works in POT textures"); 
             ccGLBindTexture2D( m_uName ); 
             glGenerateMipmap(GL_TEXTURE_2D); 
             m_bHasMipmaps = true; 
            } 
           
          
          也可以在生成圖片的時候直接創建mipmap的圖片。 etc1貌似不支持內存中直接生成。 開啓mipmap進行渲染會多30%左右的內存開銷,但是如果圖片縮小渲染的話,會提高運行效率,並且會提高畫質(直接縮小可能某些像素通過11中提到的紋理過濾計算起來會有偏差,但是使用預先縮小的圖片就可以達到自己滿意的效果)。 而etc1的話在創建圖片的時候開啓mipmap會多創建n張縮小紋理,對應文件體積就增大了,最大會增加30%~50%。 這個我們看情況使用,部分核心的重要的圖片開啓mipmap。 加載圖片成爲mipmap比較簡單 glTexImage2D(GL_TEXTURE_2D, 0, s_compressFormat_RGBA, (GLsizei)pixelsWide, (GLsizei)pixelsHigh, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); 這個是提交紋理數據的函數,其中第二是mipmap等級,穿n就對應n級的mipmap,也就是說,如果pkm的etc1圖片要支持mipmap,就需要自己寫代碼,另加載1~n張紋理,然後使用glTexImage2D這個函數把這n張紋理提交給顯卡。
         
         
         
         
         
        
          13、cocos2d-x中對etc1的支持比較初級。 既沒有透明色的支持,也不支持mipmap。
         
        
          etc1圖片格式有兩種,一種是pkm,這種是簡單的etc1格式,現在cocos2d-x支持的就是這種格式。另外一種是ktx格式,這個是opengles組織提供的官方格式。可以把多個mipmap打包到一個ktx文件裏面。 現在我的代碼裏面使用的就是ktx格式。使用ktx圖片需要到
         這裏下載ktx的loader庫,把這個庫加入到cocos2d-x中,核心加載代碼如下
         
         
          
           
            
            [cpp] 
            view plain
            copy 
             
             
            
           
           
            bool CCTextureETC::initWithKtxData(etc1_byte* pData, int len) 
            { 
             GLuint texture = 0; 
             GLenum target; 
             GLboolean isMipmapped; 
             GLenum glerror; 
             GLubyte* pKvData; 
             unsigned int kvDataLen; 
             KTX_dimensions dimensions; 
             KTX_error_code ktxerror; 
             KTX_hash_table kvtable; 
             GLint sign_s = 1, sign_t = 1; 
             
             ktxerror = ktxLoadTextureM(pData, len, &_name, &target, &dimensions, &isMipmapped, &glerror, &kvDataLen, &pKvData); 
             if (KTX_SUCCESS == ktxerror) { 
             _width = dimensions.width; 
             _height = dimensions.height; 
             
             ktxerror = ktxHashTable_Deserialize(kvDataLen, pKvData, &kvtable); 
             if (KTX_SUCCESS == ktxerror) { 
             GLubyte* pValue; 
             unsigned int valueLen; 
             
             if (KTX_SUCCESS == ktxHashTable_FindValue(kvtable, KTX_ORIENTATION_KEY, 
             &valueLen, (void**)&pValue)) 
             { 
             char s, t; 
             
             if (_snscanf((const char*)pValue, valueLen, KTX_ORIENTATION2_FMT, &s, &t) == 2) { 
             if (s == 'l') sign_s = -1; 
             if (t == 'd') sign_t = -1; 
             } 
             } 
             ktxHashTable_Destroy(kvtable); 
             free(pKvData); 
             } 
             
             // 加載成功 
             if (isMipmapped) { 
             /* Enable bilinear mipmapping */ 
             glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); 
             } else { 
             glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 
             } 
             
             glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 
             glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 
             glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 
             glBindTexture(target, 0);// 這句很重要,否則會有一些詭異的渲染問題 
             return true; 
             } 
             return false; 
            } 
發佈了44 篇原創文章 · 獲贊 9 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章