Kris

紋理,那個吃掉你視訊記憶體的大胃王

3D 壓縮紋理壓縮WebGLWebGPU

上一篇我們把頂點砍了一半,模型小了一點,但沒瘦成一道閃電——因為真正的體積大戶還在那兒:紋理。一個 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 像素,再整塊塞進視訊記憶體。

這個過程有三重麻煩:

  1. 視訊記憶體爆炸:解壓後的原始像素佔用巨大,87MB 不是誇張,是公式算出來的。
  2. 上傳阻塞:大塊像素從 CPU 記憶體搬到 GPU 視訊記憶體是個慢操作,會卡住首幀渲染。
  3. CPU 解壓開銷:大圖解壓本身就費時,行動端尤其明顯。

延續上一篇「壓縮海綿」的比喻:PNG/JPG 就是捏扁了的海綿,方便運輸;一到 GPU 上,海綿吸水膨脹成原樣。下載是快了,視訊記憶體一點沒省。

GPU 自己的紋理格式:天生就在視訊記憶體裡壓著

既然 GPU 不接受壓縮好的 PNG,那能不能讓紋理在視訊記憶體裡也保持壓縮狀態?GPU 取樣時即時解碼單個像素區塊,幾乎無開銷。

這就是 GPU 原生紋理格式(GPU-native texture format)做的事。代表家族:

格式家族全稱主要平台特點
BC1-7Block Compression桌面(PC、Mac)老牌,每代 4×4 像素區塊壓縮
ETC1/2Ericsson Texture Compression行動(Android/iOS 老裝置)行動端老標準
ASTCAdaptive Scalable Texture Compression行動/VR(新裝置)靈活,品質最好,逐區塊可調
PVRTCPowerVR老 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_basisuglTF 擴充(告訴引擎這是 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。

重點不是某一行的精確數字,而是這兩條:

  1. 傳統格式(PNG/JPG/WebP)視訊記憶體佔用幾乎一樣——都是解壓後的原始像素,87MB。磁碟再小,視訊記憶體不省。
  2. KTX2 視訊記憶體直接降到 1/4 到 1/8,且磁碟大小也並不吃虧。

這也是為什麼 VR、行動端 Web 幾乎必上 KTX2——87MB 一張貼圖在 2GB 視訊記憶體的手機上能放幾張?而 11MB 能放 7 張。

平台支援矩陣:哪些 GPU 認哪些格式

雖然 Basis 幫我們屏蔽了細節,但了解底層映射有助於排障。這是當前主流裝置對原生格式的支援情況:

平台 / 裝置BC1-7ETC2ASTCPVRTC
桌面 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 逐像素解壓」這一大塊,上傳的資料量也小一個數量級。首幀渲染快、視訊記憶體省,是這套方案的核心價值。

下一步

道理講完了,下一篇動手。我們用 toktxgltf-transform 把紋理實際壓成 KTX2,在 Three.js / Babylon.js 裡載入出來,再聊聊 ETC1S 和 UASTC 怎麼選、壓縮參數怎麼調。

贊助支持