貼圖,那個吃掉你顯存的大胃王
上一篇我們把頂點砍了一半,模型小了點,但沒瘦成一道閃電——因為真正的體積大戶還在那兒:貼圖。一個 PBR 模型,貼圖通常佔 80% 以上體積,而且這部分在顯存裡膨脹得最兇。
這一篇專治貼圖的「顯存大胃王」問題。講三件事:為什麼 PNG/JPG 在 GPU 眼裡有原罪;GPU 自己的貼圖格式長什麼樣、為什麼不能直接用;以及 Basis Universal + KTX2 這套組合是怎麼把三者打通的。
先複習一遍:為什麼 JPG 讓顯存爆炸
上一篇給過一個公式:
顯存佔用 = 寬度 * 高度 * 4位元組(RGBA) * 1.333(含mipmap)
一張 4096×4096 的貼圖,無論它在磁碟上是 1.5MB 的 JPG 還是 8MB 的 PNG,進了顯存都是 約 87MB。原因只有一個:GPU 不認識 JPG/PNG。
GPU 的貼圖取樣單元(texture sampler)只懂一件事——給定一個 UV 座標,從一個固定大小的像素塊裡讀出顏色。它要求貼圖在顯存裡是「鋪平的原始像素」。所以瀏覽器在把 JPG 上傳到 GPU 之前,必須先用 CPU 把它完全解壓縮成 RGBA 像素,再整塊塞進顯存。
這個過程有三重麻煩:
- 顯存爆炸:解壓縮後的原始像素佔用巨大,87MB 不是誇張,是公式算出來的。
- 上傳阻塞:大塊像素從 CPU 記憶體搬到 GPU 顯存是個慢操作,會卡住首幀算繪。
- CPU 解壓縮開銷:大圖解壓縮本身就費時,行動端尤其明顯。
延續上一篇「壓縮海綿」的比喻:PNG/JPG 就是捏扁了的海綿,方便運輸;一到 GPU 上,海綿吸水膨脹成原樣。下載是快了,顯存一點沒省。
GPU 自己的貼圖格式:天生就在顯存裡壓著
既然 GPU 不接受壓縮好的 PNG,那能不能讓貼圖在顯存裡也保持壓縮狀態?GPU 取樣時即時解碼單個像素塊,幾乎無開銷。
這就是 GPU 原生貼圖格式(GPU-native texture format)做的事。代表家族:
| 格式家族 | 全稱 | 主要平台 | 特點 |
|---|---|---|---|
| BC1-7 | Block Compression | 桌面(PC、Mac) | 老牌,每代 4×4 像素塊壓縮 |
| ETC1/2 | Ericsson Texture Compression | 行動(Android/iOS 舊裝置) | 行動端舊標準 |
| ASTC | Adaptive Scalable Texture Compression | 行動/VR(新裝置) | 靈活,品質最好,逐塊可調 |
| PVRTC | PowerVR | 舊 iOS | 已逐步被 ASTC 取代 |
這些格式的共同點:貼圖按 4×4 像素的小塊(block)壓縮儲存,GPU 在取樣時按需解碼這一小塊,解出來的不是單個像素而是一塊。好處是顯存佔用直接按固定比例縮水,無論內容是什麼。
對比一下:
| PNG/JPG(傳統) | GPU 原生格式 | |
|---|---|---|
| 磁碟大小 | 小(JPG 尤其小) | 中(按塊壓縮,固定碼率) |
| 顯存佔用 | 大(解壓縮回原始像素) | 小(塊狀壓縮,常駐) |
| 上傳到 GPU | 慢(CPU 解壓縮 + 大塊傳輸) | 快(直接搬,無需解壓縮) |
| 取樣速度 | 快(已是原始像素) | 快(硬體即時解碼) |
看起來 GPU 格式是完美方案。那為什麼不能直接用它們?
問題來了:不同裝置認不同格式
這正是 GPU 貼圖格式最大的坑——碎片化。
- 桌面 PC 認 BC1-7,不認 ASTC
- 安卓手機認 ETC2/ASTC,多數不認 BC
- iOS(A7 之後)認 ASTC,舊機型認 PVRTC
- WebGPU/WebGL 走的還是裝置背後的同一套硬體能力
一張貼圖,如果你想讓它「在所有裝置上都以 GPU 原生格式存在」,就得為每個平台分別準備一份。一個產品要發桌面 + 安卓 + iOS,同一張貼圖要做 BC + ETC2/ASTC 三套版本。包體翻三倍,工程量翻三倍。
更糟的是,Web 端你根本不知道使用者拿什麼裝置打開頁面。預先生成所有格式不現實,執行時偵測又來不及。
Basis Universal:一次編碼,到處轉碼
Basis Universal(簡稱 Basis)就是為解決這個碎片化而生的。它的思路一句話:
先把貼圖編碼成一種「中間格式」,執行時再根據當前裝置的 GPU 能力,轉碼(transcode)成對應的原生格式。
轉碼流程(示意圖):
原始貼圖(PNG/JPG)
│ 一次性離線編碼(慢,只做一次)
▼
Basis 中間格式(ETC1S 或 UASTC)
│ 打包進 KTX2 容器
▼
發布到 Web ──┬── 桌面 GPU ──→ 執行時轉碼 → BC1/3/7
├── Android ───→ 執行時轉碼 → ETC2
└── iOS/VR ────→ 執行時轉碼 → ASTC
關鍵點:
- 離線編碼只做一次,得到一個緊湊的中間表示
- 執行時轉碼極快(純計算、幾毫秒級),而且轉的是塊格式,不需要逐像素解壓縮
- 轉碼後送進顯存的就是真正的 GPU 原生格式,顯存佔用按塊壓縮算,和 GPU 原生格式一致
Basis 提供兩種中間編碼模式,下一篇會展開講,這裡先記住名字:
- ETC1S:壓縮率極高,適合漫反射/albedo 等顏色貼圖
- UASTC:品質更高,適合法線等對精度敏感的貼圖
KTX2:裝 GPU 貼圖的標準容器
到這裡還有一個工程問題:編碼後的 Basis 資料放哪、怎麼標記、怎麼和 glTF 關聯?答案是 KTX2。
KTX2(Khronos Texture 2)不是又一種圖片格式,而是一個容器格式——就像 .zip 不關心裡面裝的是文件還是圖片,KTX2 只負責把 GPU 貼圖資料(包括 Basis 編碼的)按標準結構打包,並附上元資訊(格式、mipmap 層級、色彩空間等)。
在 glTF 裡,KTX2 透過擴展 KHR_texture_basisu 接入:貼圖不再是 PNG 檔案,而是一個 KTX2 檔案,裡面是 Basis 編碼。載入時引擎偵測裝置能力,轉碼成對應的 BC/ETC/ASTC。
三者關係理一下,別混了:
| 名字 | 角色 | 類比 |
|---|---|---|
| Basis Universal | 編碼方案(怎麼把貼圖壓成中間格式) | 一種「壓縮演算法」 |
| KTX2 | 容器格式(怎麼把編碼後的資料打包) | 一個「包裝盒」 |
| KHR_texture_basisu | glTF 擴展(告訴引擎這是 Basis 貼圖) | 一個「標籤」 |
一個 KTX2 檔案,內部可以是 Basis 編碼(跨平台),也可以是某種原生格式(比如直接裝 BC7)。Web 上 99% 的情況是裝 Basis,因為我們要的就是「一次編碼、到處轉碼」。
VRAM 實例:4096 貼圖的對比
把前面的公式和 GPU 格式疊在一起,看一張 4096×4096 RGBA 貼圖在不同方案下的真實佔用:
| 方案 | 磁碟大小 | 顯存佔用(含 mipmap) | 上傳速度 | 跨平台 |
|---|---|---|---|---|
| PNG | ~8MB | ~87MB | 慢(需解壓縮) | ✅ |
| JPG | ~1.5MB | ~87MB | 慢(需解壓縮) | ✅ |
| WebP | ~2MB | ~87MB | 慢(需解壓縮) | ✅ |
| KTX2 (ETC1S) | ~2-3MB | ~11-14MB | 快 | ✅(轉碼) |
| KTX2 (UASTC) | ~6-8MB | ~22MB | 快 | ✅(轉碼) |
顯存數字怎麼來的:GPU 塊壓縮通常按 4bpp(每像素 4 位)或 8bpp 算。4096×4096 在 4bpp 下約 8MB,含 mipmap 再乘 1.333 ≈ 11MB。UASTC 多數轉碼到 8bpp,所以約 22MB。
重點不是某一行的精確數字,而是這兩條:
- 傳統格式(PNG/JPG/WebP)顯存佔用幾乎一樣——都是解壓縮後的原始像素,87MB。磁碟再小,顯存不省。
- KTX2 顯存直接降到 1/4 到 1/8,且磁碟大小也並不吃虧。
這也是為什麼 VR、行動端 Web 幾乎必上 KTX2——87MB 一張貼圖在 2GB 顯存的手機能放幾張?而 11MB 能放 7 張。
平台支援矩陣:哪些 GPU 認哪些格式
雖然 Basis 幫我們屏蔽了細節,但了解底層映射有助於排障。這是目前主流裝置對原生格式的支援情況:
| 平台 / 裝置 | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| 桌面 PC(D3D11/12、Vulkan、WebGPU) | ✅ | ❌ | 部分(新 GPU) | ❌ |
| macOS(Metal) | ✅(新機型) | ❌ | ✅ | ❌ |
| Android(主流) | ❌ | ✅ | ✅ | ❌ |
| iOS(A8+) | ❌ | ✅ | ✅ | ✅(舊機型) |
| WebGL 2 | 取決於擴展 | ✅ | 部分 | ❌ |
| WebGPU | ✅(桌面) | ✅ | ✅(視裝置) | ❌ |
Basis 在執行時會探測這些能力,把同一份中間編碼轉成最合適的那一個。這也是為什麼 Basis 這一層在 Web 上幾乎不可替代——你沒法在發布前預知使用者裝置。
上傳流程對比:傳統 vs GPU 格式
最後用一張流程圖把差別固化下來。
傳統 PNG/JPG:
PNG 檔案 ──下載──> CPU 記憶體 ──CPU 解壓縮(慢)──> RGBA 像素塊 ──上傳(大,慢)──> 顯存(87MB)
KTX2 + Basis:
KTX2 檔案 ──下載──> CPU 記憶體 ──執行時轉碼(快)──> GPU 塊格式 ──上傳(小,快)──> 顯存(11MB)
後者少了「CPU 逐像素解壓縮」這一大塊,上傳的資料量也小一個數量級。首幀算繪快、顯存省,是這套方案的核心價值。
下一步
道理講完了,下一篇動手。我們用 toktx、gltf-transform 把貼圖實際壓成 KTX2,在 Three.js / Babylon.js 裡載入出來,再聊聊 ETC1S 和 UASTC 怎麼選、壓縮參數怎麼調。