Kris

Texture, i divoratori di VRAM

Compressione 3DCompressione textureWebGLWebGPU

Nell'articolo precedente abbiamo dimezzato i vertici, il modello è diventato più piccolo, ma non è diventato magro come un fulmine – perché il vero peso massimo è ancora lì: le texture. In un modello PBR, le texture occupano di solito oltre l'80% del volume, e questa parte è quella che si gonfia di più nella VRAM.

Questo articolo si concentra sul problema del "divoratore di VRAM" delle texture. Parleremo di tre cose: perché PNG/JPG sono peccato originale per la GPU; come sono fatti i formati nativi di texture della GPU e perché non possiamo usarli direttamente; e come la combinazione Basis Universal + KTX2 riesce a mettere d'accordo tutto.

Ricapitoliamo: perché JPG fa esplodere la VRAM

Nell'articolo precedente abbiamo dato una formula:

Occupazione VRAM = larghezza * altezza * 4 byte (RGBA) * 1.333 (con mipmap)

Una texture 4096×4096, che sul disco è 1.5 MB in JPG o 8 MB in PNG, nella VRAM diventa circa 87 MB. Il motivo è uno solo: la GPU non riconosce JPG/PNG.

L'unità di campionamento texture (texture sampler) della GPU sa solo una cosa: dato un coordinate UV, leggere il colore da un blocco di pixel di dimensione fissa. Richiede che la texture nella VRAM sia "pixel grezzi distesi". Quindi il browser, prima di caricare un JPG sulla GPU, deve decomprimerlo completamente con la CPU in pixel RGBA, e poi spedire tutto il blocco nella VRAM.

Questo processo ha tre problemi:

  1. Esplosione della VRAM: i pixel grezzi decompressi occupano un'enorme quantità di spazio; 87 MB non è un'esagerazione, è il risultato della formula.
  2. Blocco durante il caricamento: spostare grossi blocchi di pixel dalla memoria della CPU alla VRAM della GPU è un'operazione lenta, che blocca il rendering del primo frame.
  3. Overhead di decompressione della CPU: decomprimere immagini grandi richiede tempo, soprattutto sui dispositivi mobili.

Continuando con la metafora della "spugna compressa" dell'articolo precedente: PNG/JPG sono spugne strizzate, facili da trasportare; una volta arrivate sulla GPU, la spugna assorbe acqua e si gonfia come prima. Il download è più veloce, ma la VRAM non è stata risparmiata per niente.

Formati di texture nativi della GPU: compressi per natura nella VRAM

Dal momento che la GPU non accetta PNG/JPG compressi, possiamo far sì che le texture rimangano compresse anche nella VRAM? La GPU decodifica in tempo reale singoli blocchi di pixel durante il campionamento, quasi senza overhead.

Questo è ciò che fanno i formati di texture nativi della GPU (GPU-native texture format). Famiglie rappresentative:

FamigliaNome completoPiattaforma principaleCaratteristiche
BC1-7Block CompressionDesktop (PC, Mac)Storico, compressione a blocchi 4×4 pixel per generazione
ETC1/2Ericsson Texture CompressionMobile (Android/iOS vecchi)Standard vecchio per mobile
ASTCAdaptive Scalable Texture CompressionMobile/VR (dispositivi nuovi)Flessibile, qualità migliore, regolabile per blocco
PVRTCPowerVRiOS vecchiSostituito gradualmente da ASTC

Il punto comune di questi formati: la texture viene compressa in piccoli blocchi (block) di 4×4 pixel, e la GPU decodifica solo quel blocco quando necessario, non un singolo pixel ma un blocco intero. Il vantaggio è che l'occupazione della VRAM si riduce di un rapporto fisso, indipendentemente dal contenuto.

Confronto:

PNG/JPG (tradizionale)Formato nativo GPU
Dimensione su discoPiccola (JPG soprattutto)Media (blocchi compressi, bitrate fisso)
Occupazione VRAMGrande (decompresso in pixel grezzi)Piccola (compressione a blocchi, permanente)
Caricamento sulla GPULento (decompressione CPU + trasferimento grande)Veloce (trasferimento diretto, senza decompressione)
Velocità di campionamentoVeloce (già pixel grezzi)Veloce (decodifica hardware in tempo reale)

Sembra che i formati GPU siano la soluzione perfetta. Allora perché non possiamo usarli direttamente?

Il problema: dispositivi diversi riconoscono formati diversi

Questo è il grande scoglio dei formati di texture GPU: frammentazione.

  • PC desktop riconoscono BC1-7, non ASTC
  • Smartphone Android riconoscono ETC2/ASTC, la maggior parte non riconosce BC
  • iOS (dopo A7) riconosce ASTC, vecchi modelli riconoscono PVRTC
  • WebGPU/WebGL si basano sulle stesse capacità hardware del dispositivo sottostante

Una texture, se vuoi che "esista in formato nativo GPU su tutti i dispositivi", devi preparare una versione per ogni piattaforma. Un prodotto che deve essere distribuito su desktop + Android + iOS richiede tre set di texture: BC + ETC2/ASTC. Il volume del pacchetto triplica, il lavoro triplica.

Peggio ancora, sul Web non sai mai con quale dispositivo l'utente apre la pagina. Generare tutti i formati in anticipo non è realistico, e rilevare al runtime è troppo tardi.

Basis Universal: una codifica, ovunque transcodifica

Basis Universal (abbreviato Basis) è nato per risolvere questa frammentazione. La sua idea in una frase:

Prima si codifica la texture in un "formato intermedio", poi al runtime, in base alle capacità GPU del dispositivo corrente, si transcodifica (transcode) nel formato nativo corrispondente.

Flowchart di transcodifica (schematico):

Texture sorgente (PNG/JPG)
      │  Codifica offline una tantum (lenta, una sola volta)
      ▼
Formato intermedio Basis (ETC1S o UASTC)
      │  Impacchettato in contenitore KTX2
      ▼
Pubblicato sul Web ──┬── GPU desktop ──→ transcodifica runtime → BC1/3/7
                    ├── Android ──→ transcodifica runtime → ETC2
                    └── iOS/VR ──→ transcodifica runtime → ASTC

Punti chiave:

  • La codifica offline viene eseguita una sola volta, ottenendo una rappresentazione intermedia compatta
  • La transcodifica runtime è estremamente veloce (pura computazione, pochi millisecondi), e transcodifica in formato a blocchi, non richiede decompressione pixel per pixel
  • Dopo la transcodifica, ciò che entra nella VRAM è il vero formato nativo GPU, l'occupazione è calcolata in base alla compressione a blocchi, identica al formato nativo

Basis offre due modalità di codifica intermedia, che approfondiremo nel prossimo articolo, ma ecco i nomi:

  • ETC1S: rapporto di compressione altissimo, adatto per mappe di colore diffuse/albedo
  • UASTC: qualità superiore, adatto per normal map e altre texture sensibili alla precisione

KTX2: il contenitore standard per texture GPU

A questo punto c'è un problema di ingegneria: dove mettere i dati Basis codificati? Come etichettarli? Come associarli a glTF? La risposta è KTX2.

KTX2 (Khronos Texture 2) non è un altro formato di immagine, ma un formato contenitore – proprio come .zip non si preoccupa di cosa contiene, KTX2 si occupa di impacchettare i dati di texture GPU (inclusi quelli Basis) in una struttura standard, allegando metadati (formato, livelli mipmap, spazio colore, ecc.).

In glTF, KTX2 viene integrato tramite l'estensione KHR_texture_basisu: la texture non è più un file PNG, ma un file KTX2 contenente la codifica Basis. Al caricamento, il motore rileva le capacità del dispositivo e transcodifica nel corrispondente BC/ETC/ASTC.

Chiariamo le relazioni, non confondiamole:

NomeRuoloAnalogia
Basis UniversalSchema di codifica (come comprimere la texture in formato intermedio)Un "algoritmo di compressione"
KTX2Formato contenitore (come impacchettare i dati codificati)Una "scatola da imballaggio"
KHR_texture_basisuEstensione glTF (dice al motore che è una texture Basis)Una "etichetta"

Un file KTX2 può contenere internamente codifica Basis (cross-platform) o un formato nativo (ad esempio BC7 direttamente). Sul Web, nel 99% dei casi contiene Basis, perché vogliamo "codifica una volta, transcodifica ovunque".

Esempio VRAM: confronto texture 4096

Combinando le formule precedenti con i formati GPU, vediamo l'occupazione reale di una texture RGBA 4096×4096 con diversi schemi:

SchemaDimensione su discoOccupazione VRAM (con mipmap)Velocità di caricamentoCross-platform
PNG~8 MB~87 MBLento (decompressione)
JPG~1.5 MB~87 MBLento (decompressione)
WebP~2 MB~87 MBLento (decompressione)
KTX2 (ETC1S)~2–3 MB~11–14 MBVeloce✅ (transcodifica)
KTX2 (UASTC)~6–8 MB~22 MBVeloce✅ (transcodifica)

Come si ottengono i numeri della VRAM: la compressione a blocchi GPU è solitamente calcolata a 4 bpp (bit per pixel) o 8 bpp. 4096×4096 a 4 bpp è circa 8 MB, con mipmap moltiplica per 1.333 ≈ 11 MB. UASTC transcodifica per lo più a 8 bpp, quindi circa 22 MB.

Il punto non sono i numeri esatti di una riga, ma queste due linee:

  1. L'occupazione VRAM dei formati tradizionali (PNG/JPG/WebP) è quasi identica – tutti decompressi in pixel grezzi, 87 MB. Per quanto il disco sia piccolo, la VRAM non viene risparmiata.
  2. **KTX2 riduce la VRAM direttamente a 1/
Supportaci