紋理,那個吃掉你視訊記憶體的大胃王
上一篇我們把頂點砍了一半,模型小了一點,但沒瘦成一道閃電——因為真正的體積大戶還在那兒:紋理。一個 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 怎麼選、壓縮參數怎麼調。