關於圖片渲染的過程


  挺久以前有個騰訊的面試, 也是基於游戲引擎開發東西的, 問到一個圖片是怎樣加載渲染的, 我最后說到壓縮的圖片直接進到GPU, 通過GPU解壓獲取最終像素, 然后被連着問了兩次你確定?

  然后我就有點迷糊了, 不是嗎? 然后他也沒說他為啥這樣問, 或者想要的是什么答案, 就略了

  現在回想起來, 可能是對於過程的理解或者平台的不同, 也會造成雞同鴨講的結果的吧...這里再把圖片從創建資源到加載渲染的流程捋一遍.

 

  最原始的位圖文件BMP, 常見的壓縮格式 JPG / PNG , 不常用格式 ETC / PVR 這些 : 

  它們都是同一張圖片, 只是存的類型不一樣, 位圖文件就是RGB直接存的, 大小直接就能算出來 : 

  627 * 310 * 3Byte = 583,110 B => 571KB

  可是相比壓縮格式它占的空間實在太大了, 跟 JPG 差了幾十倍, 不過 JPG 是有損壓縮方式壓縮的, 當解壓回來后圖像會損失信息(網絡一些圖片盜來盜去, 越來越綠就是這個原因), PNG 是無損壓縮的, 解壓回來還是那個樣子, 所以根據不同情況基本這兩種壓縮就涵蓋了有損和無損區別了.

  而 ETC / PVR 這些也是有損壓縮, 而它們的計算邏輯跟 JPG 也是不同的, ETC / PVR 對應於 GPU 硬件解壓邏輯, 我們都知道GPU是並行單元的, 就像超市存包的鎖櫃, 每個櫃子直接用鑰匙打開就能取東西了, 可以同時 存/取 N個包, ECT / PVR 的解壓是基於一個全局查找表邏輯的, 取出來的結果對照查找表然后進行調制就能得到像素值了:

 BMP -> ETC / PVR   在內存上相當於縮小的BMP, 差不多除以6

  定長壓縮, 直接就能找到對應像素點的 Block, 因為 GPU 很猛, 解壓計算很快, 就跟直接讀取位圖一樣沒有差別. 所以一般 ETC / PVR 圖片還是保持壓縮格式躺在 GPU 的內存里. 下面是非常直觀的PVRTC還原像素過程 : 

  而 JPG / PNG 這些非定長壓縮的, 你並不能直接從像素推斷獲取它對應像素點的數據在內存中的位置, GPU 就沒有辦法並行的去找內存然后解壓對應像素信息了, 比如 JPG 自帶了數據壓縮 : 

  所以一個 JPG 的解壓步驟很多, 把這些用 GPU 實現的話相當困難, 主要是算法復雜 : 

   上面說的是格式的壓縮, 還有另外一個文件的壓縮.

  GPU 部分的差別大概就是這些了, 然后打包游戲資源的時候, 一般還會把這些壓縮格式圖片進行一次打包, 而這個打包也會進行數據壓縮, 看看打包壓縮對它們的影響 : 

  可以看到壓縮對它們還是有很大影響的, JPG / PNG 自帶了去冗余過程, 可壓縮量不大, 甚至 PNG 反而文件變更大了, 而原始 BMP 文件可壓縮量很可觀, 當然 ETC / PVR 這些跟BMP 有點類似, 也是能壓縮的 :

  這是PC上的 DXT 壓縮格式打的 AssetBundle 包, 它的 DXT 內存大小是顯示在檢視面板上的 : 

  而如果不壓縮, 它在內存上的大小是這樣的 : 

 PS : 這里為了符合GPU壓縮格式, 打開 Power Of 2 了, 圖片尺寸被限制到了 512x256 跟原始圖片 627x310 大小有點差距, 不過不影響判斷

 

  這樣看下來, 其實解壓就有兩種意思了, 一個是文件解壓, 像上面的 zip 解壓出圖片文件, 然后是像素解壓, 是把壓縮格式的圖片還原為像素的過程, 比如 NPG 文件還原為 BMP 格式然后傳入 GPU, GPU 就躺着讀取就行了, 還有就是 GPU 支持的壓縮格式 DXT / ETC / PVR 等, 只需要把壓縮格式圖片傳給GPU, 讓 GPU 大大的干活就行了.

  然后文件解壓階段, 必然是由 CPU 進行的, 比如加載 AssetBundle, 它就會在讀取完文件之后進行解壓操作(一般情況), 如果基於塊壓縮的(ChunkBasedCompression)它可能在讀取相關資源的時候再對指定資源解壓( AssetBundle.LoadAsset() ).

  最后是 GPU 解壓階段 :

  1. 如果 GPU 不支持你的壓縮格式, 引擎會用 CPU 把圖片解壓成位圖文件, 然后再傳給 GPU, 這樣又浪費 CPU 資源, 又浪費總線帶寬

  2. 如果支持的話, 就直接把壓縮格式的圖片傳輸給 GPU 了, 可以說是由 GPU 進行解壓光柵化. 

  3. 復雜的壓縮格式 JPG / PNG 基本不可能被 GPU 支持, 幾乎都由 CPU 解壓成位圖傳遞給 GPU的

  所以游戲引擎才會直接把扔進去的圖片都進行對應平台的轉換, 因為追求的是效率和壓榨硬件性能, 而 Web 網頁這種跨平台又是網絡傳輸的, 一般都使用 JPG / PNG 這些自帶壓縮的格式, 雖然解壓過程比較復雜. 下面是總結的流程圖 : 

   

  左邊就是通過 CPU 解壓然后傳遞到 GPU 的過程, 可以看到它在系統中解壓, 然后把非壓縮的圖片傳遞給了 GPU, 它的好處是任何平台都能通用, 並且可以使用算法復雜的壓縮格式比如 JPG / PNG 等, 又或者當前壓縮格式不是 GPU 能夠解壓的, 比如 PVRTC 格式放到安卓機子上, 它只能通過 CPU 解壓之后傳遞位圖給 GPU, 我們常說的 ETC2 格式在老的安卓機上會在 CPU 上解壓成位圖傳遞到 GPU 就是這個道理, 因為老的安卓機只能支持 ETC1, 然后 CPU 解壓造成性能瓶頸.

  右邊就是非常理想的通過 GPU 解壓的例子, 比如 ETC1 的圖片跑在安卓機子上, PVR 的圖片跑在 Meta 上, DXT5 的跑在 DX11 上, 既節省了傳輸的帶寬, 又能利用 GPU 的並行解壓, 壓榨硬件性能.

 

  這樣回到最初的那個問答, 本地資源或是 AssetBundle 中讀取出來的圖片, 正常來說還是那樣, 圖片保持壓縮格式進入 GPU 解壓, 或者叫 Decode 得到像素值, 不過如果從 WWW 之類的獲取到的 PNG / JPG 圖片, 那就會在 CPU 中進行解壓, 然后位圖傳入 GPU.

  再加一個 WWW 下載來的圖片跟本地圖片加載后的差別吧, 上面的已經看到了本地圖片內存占用 64KB(DXT), 而本地位圖的話, 占570KB, 同樣的圖片放到服務器然后看看 : 

public class Test : MonoBehaviour
{
    public UnityEngine.UI.RawImage img;

    void Start()
    {
        StartCoroutine(Load(@"http://127.0.0.1:44599/StreamingAssets/PNG001.PNG"));
    }

    IEnumerator Load(string url)
    {
        var www = new WWW(url);
        yield return www;
        img.texture = www.texture;
        img.texture.name = "PNG001.PNG";
    }
}

  變成2M了, 這是發布后的 exe 運行得到的數據. 

  按照理論計算這張圖片的位圖大小應該是 627x310x4 = 777KB (按照32位計算), 就算加上 Read/Write 屬性, 也是 777 * 2 = 1.5MB, 不知道 Texture2D 的額外開銷在哪, 反正是對 CPU 解壓很不友好的了.

 

  最近也看到了一些調用英偉達接口實現 JPEG 壓縮/解壓的, 比如這里提到的 : http://blog.csdn.net/weixinhum/article/details/46683509 ( CUDA 實現JPEG圖像解碼為RGB數據 )

  不過這些都不是業界標准接口, 沒有太多意義, 並且不是在 GPU 上實現的, 始終只是通過 GPU 提供輔助 壓縮/解壓 服務給 CPU 調用, 最終還是走的 CPU 解壓的路子, 不過反過來看, 它可以給視頻流提供 GPU 的解壓輔助, 對各種需要高速解壓識別的圖像應該有驚喜...

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM