Texturas, ese gran devorador de tu VRAM
En el artículo anterior redujimos los vértices a la mitad, el modelo se volvió más pequeño, pero no se adelgazó como un rayo, porque el verdadero gran consumidor de volumen sigue ahí: las texturas. En un modelo PBR, las texturas suelen ocupar más del 80% del volumen, y además se expanden de forma más agresiva en la VRAM.
Este artículo se centra en el problema del «gran devorador de VRAM» de las texturas. Vamos a tratar tres cosas: por qué PNG/JPG son «pecados originales» a los ojos de la GPU; cómo son los formatos de textura propios de la GPU y por qué no se pueden usar directamente; y cómo el combo Basis Universal + KTX2 logra unificar los tres.
Repaso: por qué JPG hace explotar la VRAM
En el artículo anterior dimos una fórmula:
Ocupación de VRAM = ancho * alto * 4 bytes (RGBA) * 1.333 (con mipmaps)
Una textura de 4096×4096, ya sea un JPG de 1.5 MB en disco o un PNG de 8 MB, ocupa en la VRAM unos 87 MB. Solo hay una razón: la GPU no entiende JPG/PNG.
La unidad de muestreo de textura (texture sampler) de la GPU solo sabe hacer una cosa: dado un par de coordenadas UV, leer el color de un bloque de píxeles de tamaño fijo. Exige que la textura esté en la VRAM como «píxeles originales sin comprimir». Por lo tanto, antes de subir el JPG a la GPU, el navegador debe descomprimirlo completamente en píxeles RGBA usando la CPU, y luego enviar todo el bloque a la VRAM.
Este proceso conlleva tres problemas:
- Explosión de VRAM: los píxeles originales descomprimidos ocupan mucho espacio; 87 MB no es una exageración, es el resultado de la fórmula.
- Bloqueo en la subida: mover un gran bloque de píxeles de la memoria de la CPU a la VRAM de la GPU es una operación lenta, que puede bloquear el renderizado del primer fotograma.
- Coste de descompresión en CPU: descomprimir imágenes grandes lleva tiempo, especialmente en dispositivos móviles.
Continuando con la metáfora de la «esponja comprimida» del artículo anterior: PNG/JPG son esponjas aplastadas, fáciles de transportar; en cuanto llegan a la GPU, la esponja absorbe agua y se expande a su tamaño original. La descarga es rápida, pero la VRAM no se ahorra nada.
Formatos de textura nativos de la GPU: comprimidos de por vida en la VRAM
Ya que la GPU no acepta PNG comprimidos, ¿podríamos mantener la textura comprimida también en la VRAM? La GPU, al muestrear, descomprime bloques de píxeles individuales en tiempo real, casi sin sobrecarga.
Eso es lo que hacen los formatos de textura nativos de la GPU (GPU-native texture format). Familias representativas:
| Familia | Nombre completo | Plataforma principal | Característica |
|---|---|---|---|
| BC1-7 | Block Compression | Escritorio (PC, Mac) | Clásico, comprime bloques de 4×4 píxeles |
| ETC1/2 | Ericsson Texture Compression | Móvil (Android/iOS antiguo) | Estándar antiguo en móviles |
| ASTC | Adaptive Scalable Texture Compression | Móvil/VR (nuevos dispositivos) | Flexible, mejor calidad, ajustable por bloque |
| PVRTC | PowerVR | iOS antiguo | Reemplazado gradualmente por ASTC |
El punto común de estos formatos: la textura se almacena comprimida en bloques (blocks) de 4×4 píxeles, y la GPU decodifica ese bloque bajo demanda al muestrear, obteniendo no un solo píxel sino un bloque. La ventaja es que la ocupación de VRAM se reduce directamente en una proporción fija, independientemente del contenido.
Comparación:
| PNG/JPG (tradicional) | Formato nativo de GPU | |
|---|---|---|
| Tamaño en disco | Pequeño (JPG especialmente) | Medio (compresión por bloque, tasa fija) |
| Ocupación en VRAM | Grande (descomprimido a píxeles originales) | Pequeña (comprimido por bloque, permanente) |
| Subida a la GPU | Lenta (descompresión en CPU + transferencia grande) | Rápida (transferencia directa, sin descompresión) |
| Velocidad de muestreo | Rápida (ya son píxeles originales) | Rápida (decodificación hardware en tiempo real) |
Parece que los formatos de GPU son la solución perfecta. Entonces, ¿por qué no se pueden usar directamente?
El problema: distintos dispositivos reconocen distintos formatos
Precisamente ese es el mayor inconveniente de los formatos de textura de GPU: la fragmentación.
- Los PC de escritorio reconocen BC1-7, pero no ASTC.
- Los teléfonos Android reconocen ETC2/ASTC, la mayoría no reconocen BC.
- iOS (desde A7) reconoce ASTC, los modelos antiguos reconocen PVRTC.
- WebGPU/WebGL usan la misma capacidad hardware subyacente del dispositivo.
Si quieres que una textura «exista en formato nativo de GPU en todos los dispositivos», tienes que preparar una versión para cada plataforma. Un producto que se lance en escritorio + Android + iOS necesita tres versiones de la misma textura: BC + ETC2/ASTC. El paquete se triplica, el trabajo se triplica.
Peor aún, en la web no sabes qué dispositivo usará el usuario para abrir la página. Pre-generar todos los formatos no es realista, y detectarlo en tiempo de ejecución no da tiempo.
Basis Universal: una codificación, transcodificación a cualquier sitio
Basis Universal (abreviado Basis) nació para resolver esta fragmentación. Su idea se resume en una frase:
Primero codifica la textura en un «formato intermedio», y luego, en tiempo de ejecución, según las capacidades de la GPU del dispositivo, la transcodifica (transcode) al formato nativo correspondiente.
Flujo de transcodificación (esquema):
Textura original (PNG/JPG)
│ Codificación offline única (lenta, se hace una vez)
▼
Formato intermedio Basis (ETC1S o UASTC)
│ Empaquetado en contenedor KTX2
▼
Publicado en Web ──┬── GPU de escritorio ──→ transcodificación en tiempo real → BC1/3/7
├── Android ───→ transcodificación en tiempo real → ETC2
└── iOS/VR ────→ transcodificación en tiempo real → ASTC
Puntos clave:
- La codificación offline se hace una sola vez, obteniendo una representación intermedia compacta.
- La transcodificación en tiempo real es extremadamente rápida (puramente computacional, del orden de milisegundos), y transforma en formato de bloque, sin necesidad de descompresión píxel a píxel.
- Una vez transcodificada, se envía a la VRAM en el formato nativo de la GPU real, la ocupación de VRAM se calcula por compresión de bloque, idéntica al formato nativo de GPU.
Basis ofrece dos modos de codificación intermedia, que se detallarán en el próximo artículo, pero aquí recordemos sus nombres:
- ETC1S: tasa de compresión muy alta, ideal para mapas de color difuso/albedo.
- UASTC: mayor calidad, adecuado para mapas que requieren precisión, como normales.
KTX2: el contenedor estándar para texturas de GPU
Aquí surge otro problema de ingeniería: ¿dónde se colocan los datos codificados con Basis, cómo se marcan y cómo se asocian con glTF? La respuesta es KTX2.
KTX2 (Khronos Texture 2) no es otro formato de imagen, sino un formato contenedor — como .zip no se preocupa de si contiene documentos o imágenes, KTX2 se encarga de empaquetar los datos de textura de GPU (incluyendo la codificación Basis) en una estructura estándar, acompañada de metadatos (formato, niveles de mipmap, espacio de color, etc.).
En glTF, KTX2 se integra mediante la extensión KHR_texture_basisu: la textura ya no es un archivo PNG, sino un archivo KTX2 que contiene codificación Basis. Al cargar, el motor detecta las capacidades del dispositivo y transcodifica a BC/ETC/ASTC correspondiente.
Aclaremos la relación de los tres, no los confundamos:
| Nombre | Rol | Analogía |
|---|---|---|
| Basis Universal | Esquema de codificación (cómo comprimir la textura en un formato intermedio) | Un «algoritmo de compresión» |
| KTX2 | Formato contenedor (cómo empaquetar los datos codificados) | Una «caja de embalaje» |
| KHR_texture_basisu | Extensión de glTF (indica al motor que es una textura Basis) | Una «etiqueta» |
Un archivo KTX2 puede contener internamente codificación Basis (multiplataforma) o algún formato nativo (por ejemplo, BC7 directamente). En la web, el 99% de los casos es Basis, porque queremos «una codificación, transcodificación a cualquier sitio».
Ejemplo de VRAM: comparativa de una textura de 4096
Apliquemos la fórmula anterior junto con los formatos de GPU para ver la ocupación real de una textura RGBA de 4096×4096 en diferentes esquemas:
| Esquema | Tamaño en disco | Ocupación en VRAM (con mipmaps) | Velocidad de subida | Multiplataforma |
|---|---|---|---|---|
| PNG | ~8 MB | ~87 MB | Lenta (requiere descompresión) | ✅ |
| JPG | ~1.5 MB | ~87 MB | Lenta (requiere descompresión) | ✅ |
| WebP | ~2 MB | ~87 MB | Lenta (requiere descompresión) | ✅ |
| KTX2 (ETC1S) | ~2-3 MB | ~11-14 MB | Rápida | ✅ (transcod.) |
| KTX2 (UASTC) | ~6-8 MB | ~22 MB | Rápida | ✅ (transcod.) |
¿Cómo se obtienen las cifras de VRAM? La compresión por bloque de GPU suele calcularse a 4 bpp (bits por píxel) u 8 bpp. 4096×4096 a 4 bpp son unos 8 MB, con mipmaps multiplicar por 1.333 ≈ 11 MB. UASTC se transcodifica mayoritariamente a 8 bpp, por lo que unos 22 MB.
Lo importante no son los números exactos de una fila, sino estas dos conclusiones:
- Los formatos tradicionales (PNG/JPG/WebP) tienen casi la misma ocupación de VRAM — todos son píxeles originales descomprimidos, 87 MB. Por muy pequeño que sea el disco, la VRAM no se ahorra.
- KTX2 reduce la VRAM directamente a 1/4 o 1/8, y además el tamaño en disco no es peor.
Por eso en VR y web móvil es casi obligatorio usar KTX2: ¿cuántas texturas de 87 MB caben en un móvil con 2 GB de VRAM? Mientras que con 11 MB caben 7.
Matriz de soporte de plataformas: qué GPU reconocen qué formatos
Aunque Basis nos oculta los detalles, conocer el mapeo subyacente ayuda a solucionar problemas. Esta es la compatibilidad actual de los formatos nativos en los principales dispositivos:
| Plataforma / Dispositivo | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| PC de escritorio (D3D11/12, Vulkan, WebGPU) | ✅ | ❌ | Parcial (GPU nuevas) | ❌ |
| macOS (Metal) | ✅ (nuevos) | ❌ | ✅ | ❌ |
| Android (principal) | ❌ | ✅ | ✅ | ❌ |
| iOS (A8+) | ❌ | ✅ | ✅ | ✅ (modelos antiguos) |
| WebGL 2 | Depende de extensiones | ✅ | Parcial | ❌ |
| WebGPU | ✅ (escritorio) | ✅ | ✅ (según dispositivo) | ❌ |
Basis, en tiempo de ejecución, detecta estas capacidades y transcodifica la misma codificación intermedia al formato más adecuado. Esta es la razón por la que la capa de Basis es casi insustituible en la web: no puedes saber de antemano qué dispositivo usará el usuario.
Comparativa del flujo de subida: tradicional vs formato GPU
Finalmente, fijemos la diferencia con un diagrama de flujo.
Tradicional PNG/JPG:
Archivo PNG ──descarga──> Memoria CPU ──descompresión CPU (lenta)──> Bloque de píxeles RGBA ──subida (grande, lenta)──> VRAM (87 MB)
KTX2 + Basis:
Archivo KTX2 ──descarga──> Memoria CPU ──transcodificación en tiempo real (rápida)──> Formato de bloque GPU ──subida (pequeña, rápida)──> VRAM (11 MB)
En el segundo caso falta el gran bloque de «descompresión píxel a píxel en CPU», y la cantidad de datos subidos es un orden de magnitud menor. El renderizado del primer fotograma es más rápido y se ahorra VRAM: ese es el valor central de este esquema.
Próximo paso
Explicada la teoría, el siguiente artículo será práctico. Usaremos toktx, gltf-transform para comprimir texturas reales a KTX2, las cargaremos en Three.js / Babylon.js, y hablaremos de cómo elegir entre ETC1S y UASTC, y cómo ajustar los parámetros de compresión.