Texturas, el tragón que devora tu VRAM
La última vez cortamos los vértices por la mitad y el modelo encogió un poco, pero no se convirtió en un relámpago — porque el verdadero comilón de tamaño seguía ahí: las texturas. En un modelo PBR, las texturas suelen ocupar más del 80 % del tamaño, y es la parte que más se hincha en el VRAM.
Este artículo es la cura para el «tragón de VRAM» de las texturas. Tres cosas: por qué PNG/JPG son culpables a los ojos de la GPU; qué aspecto tienen los formatos de textura nativos de GPU y por qué no se pueden usar directamente; y cómo Basis Universal + KTX2 conectan a los tres.
Repaso: por qué el JPG hace explotar el VRAM
En el artículo anterior dimos una fórmula:
Uso de VRAM = ancho * alto * 4 bytes (RGBA) * 1.333 (con mipmaps)
Una textura de 4096×4096, sea un JPG de 1,5 MB o un PNG de 8 MB en disco, se convierte en ~87 MB en el VRAM. Solo hay una razón: la GPU no entiende JPG/PNG.
El sampler de textura de la GPU entiende una sola cosa: dadas unas coordenadas UV, leer un color de un bloque de píxeles de tamaño fijo. Exige que la textura esté en el VRAM como «píxeles crudos tendidos». Así que antes de subir un JPG a la GPU, el navegador debe descomprimirlo por completo en píxeles RGBA en la CPU y luego meter el bloque entero en el VRAM.
Este proceso tiene tres problemas:
- Explosión de VRAM: los píxeles crudos descomprimidos son enormes. 87 MB no es exageración, es lo que calcula la fórmula.
- Bloqueo de subida: mover un bloque grande de píxeles de la memoria de la CPU al VRAM de la GPU es una operación lenta que bloquea el primer frame.
- Coste de decodificación en CPU: decodificar una imagen grande cuesta tiempo por sí mismo, sobre todo en móvil.
Extending la metáfora de la «esponja comprimida» de la vez pasada: PNG/JPG es una esponja prensada plana para transportarla fácil; al llegar a la GPU, la esponja absorbe agua y vuelve a su tamaño completo. La descarga se hizo más rápida; el VRAM no se ahorró nada.
Los formatos de textura propios de la GPU: comprimidos en el VRAM por naturaleza
Ya que la GPU no acepta PNG precomprimido, ¿podemos mantener la textura comprimida incluso dentro del VRAM? La GPU decodifica un solo bloque de píxeles al vuelo durante el muestreo, casi sin coste.
Eso es justo lo que hacen los formatos de textura nativos de GPU. Familias representativas:
| Familia de formato | Nombre completo | Plataformas principales | Notas |
|---|---|---|---|
| BC1-7 | Block Compression | Escritorio (PC, Mac) | Veterano, compresión de bloque 4×4 por generación |
| ETC1/2 | Ericsson Texture Compression | Móvil (Android/iOS antiguos) | Estándar móvil viejo |
| ASTC | Adaptive Scalable Texture Compression | Móvil/VR (dispositivos nuevos) | Flexible, mejor calidad, ajustable por bloque |
| PVRTC | PowerVR | iOS antiguos | Siendo desplazado por ASTC |
Estos formatos comparten un rasgo: las texturas se almacenan comprimidas en bloques pequeños de 4×4 píxeles (blocks), y la GPU decodifica un bloque pequeño bajo demanda al muestrear — lo que sale no es un píxel individual sino un bloque entero. La ventaja es que el uso de VRAM se reduce en una proporción fija independientemente del contenido.
Comparación:
| PNG/JPG (tradicional) | Formatos nativos de GPU | |
|---|---|---|
| Tamaño en disco | Pequeño (JPG sobre todo) | Medio (compresión por bloque, bitrate fijo) |
| Uso de VRAM | Grande (descomprimido a píxeles crudos) | Pequeño (compresión por bloque, residente) |
| Subida a GPU | Lenta (decode CPU + transferencia grande) | Rápida (simplemente mover, sin decode) |
| Velocidad de muestreo | Rápida (ya son píxeles crudos) | Rápida (decodificación hardware en tiempo real) |
Los formatos de GPU parecen la solución perfecta. Entonces, ¿por qué no usarlos directamente?
El problema: dispositivos distintos reconocen formatos distintos
Esa es la mayor trampa de los formatos de textura de GPU — fragmentación.
- Los PC de escritorio reconocen BC1-7, no ASTC
- Los móviles Android reconocen ETC2/ASTC, la mayoría no BC
- iOS (A7+) reconoce ASTC, los viejos PVRTC
- WebGPU/WebGL se apoyan en las mismas capacidades de hardware detrás del dispositivo
Si quieres que una textura «exista como formato nativo de GPU en todos los dispositivos», tienes que preparar una copia separada para cada plataforma. Un producto para escritorio + Android + iOS necesita BC + ETC2/ASTC para la misma textura — tres versiones. El paquete se triplica, el esfuerzo también.
Peor aún: en la web no sabes qué dispositivo abre la página el usuario. Pre-generar todos los formatos es irreal, y la detección en runtime llega tarde.
Basis Universal: codifica una vez, transcodifica en todas partes
Basis Universal (Basis para abreviar) nació para resolver esta fragmentación. Su idea en una frase:
Primero codifica la textura en un «formato intermedio», luego en runtime transcodifícala al formato nativo correspondiente según las capacidades de GPU del dispositivo actual.
Flujo de transcodificación (esquemático):
Textura fuente (PNG/JPG)
│ codificación offline única (lenta, una vez)
▼
Formato intermedio Basis (ETC1S o UASTC)
│ empaquetado en un contenedor KTX2
▼
Publicar a la web ──┬── GPU escritorio ──→ transcodificar en runtime → BC1/3/7
├── Android ────→ transcodificar en runtime → ETC2
└── iOS/VR ─────→ transcodificar en runtime → ASTC
Puntos clave:
- La codificación offline ocurre una vez y produce una representación intermedia compacta
- La transcodificación en runtime es muy rápida (cálculo puro, unos milisegundos), y transcodifica formatos de bloque — no necesita descompresión por píxel
- Lo que entra al VRAM tras la transcodificación es un formato nativo de GPU real, de modo que el uso de VRAM se calcula con tasas de compresión por bloque, idéntico a los formatos nativos de GPU
Basis ofrece dos modos de codificación intermedia; el próximo artículo los despliega, pero recuerda los nombres:
- ETC1S: razón de compresión extremadamente alta, adecuado para diffuse/albedo y otros color maps
- UASTC: mayor calidad, adecuado para normales y otros maps sensibles a la precisión
KTX2: el contenedor estándar para texturas de GPU
Queda una cuestión de ingeniería: ¿dónde poner los datos Basis codificados, cómo marcarlos y cómo relacionarlos con glTF? La respuesta es KTX2.
KTX2 (Khronos Texture 2) no es otro formato de imagen — es un formato contenedor. Igual que un .zip no se preocupa de si lleva documentos o imágenes, KTX2 solo empaqueta datos de textura de GPU (incluyendo los codificados con Basis) en una estructura estándar con metadatos (formato, niveles de mipmap, espacio de color, etc.).
En glTF, KTX2 se conecta 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 al BC/ETC/ASTC correspondiente.
Desenredemos los tres roles — no los confundas:
| Nombre | Rol | Analogía |
|---|---|---|
| Basis Universal | Esquema de codificación (cómo comprimir una textura al formato intermedio) | Una especie de «algoritmo de compresión» |
| KTX2 | Formato contenedor (cómo empaquetar los datos codificados) | Una «caja» |
| KHR_texture_basisu | Extensión glTF (le dice al motor que es una textura Basis) | Una «etiqueta» |
Un archivo KTX2 puede contener codificación Basis (multiplataforma) o un formato nativo (p. ej. BC7 crudo). En la web, el 99 % de las veces contiene Basis, porque lo que queremos es «codificar una vez, transcodificar en todas partes».
Ejemplo de VRAM: una textura 4096 comparada
Apilando la fórmula y los formatos de GPU, aquí la huella real de una textura 4096×4096 RGBA bajo distintas opciones:
| Opción | Tamaño en disco | Uso de VRAM (con mipmaps) | Velocidad de subida | Multiplataforma |
|---|---|---|---|---|
| PNG | ~8 MB | ~87 MB | Lenta (necesita decode) | ✅ |
| JPG | ~1.5 MB | ~87 MB | Lenta (necesita decode) | ✅ |
| WebP | ~2 MB | ~87 MB | Lenta (necesita decode) | ✅ |
| KTX2 (ETC1S) | ~2-3 MB | ~11-14 MB | Rápida | ✅ (transcode) |
| KTX2 (UASTC) | ~6-8 MB | ~22 MB | Rápida | ✅ (transcode) |
De dónde salen los números de VRAM: la compresión por bloque de GPU suele contarse a 4bpp (4 bits por píxel) u 8bpp. 4096×4096 a 4bpp son unos 8 MB, ×1.333 con mipmaps ≈ 11 MB. UASTC transcodifica mayoritariamente a 8bpp, así que unos 22 MB.
Lo importante no es el número exacto de una fila, sino estos dos puntos:
- Los formatos tradicionales (PNG/JPG/WebP) tienen un uso de VRAM casi idéntico — todos son píxeles crudos descomprimidos, 87 MB. Por muy pequeños en disco, el VRAM no se ahorra.
- KTX2 baja el VRAM a 1/4 o 1/8, y el tamaño en disco también es competitivo.
Por eso VR y la web móvil casi siempre van con KTX2 — ¿cuántas texturas de 87 MB caben en un VRAM de 2 GB de un móvil? A 11 MB caben siete.
Matriz de soporte de plataforma: qué GPUs reconocen qué formatos
Basis nos oculta los detalles, pero entender el mapeo subyacente ayuda a resolver problemas. Aquí el soporte actual de los dispositivos principales para formatos nativos:
| Plataforma / dispositivo | BC1-7 | ETC2 | ASTC | PVRTC |
|---|---|---|---|---|
| PC escritorio (D3D11/12, Vulkan, WebGPU) | ✅ | ❌ | Parcial (GPUs nuevas) | ❌ |
| macOS (Metal) | ✅ (máquinas nuevas) | ❌ | ✅ | ❌ |
| Android (mainstream) | ❌ | ✅ | ✅ | ❌ |
| iOS (A8+) | ❌ | ✅ | ✅ | ✅ (dispositivos antiguos) |
| WebGL 2 | Según extensión | ✅ | Parcial | ❌ |
| WebGPU | ✅ (escritorio) | ✅ | ✅ (según dispositivo) | ❌ |
Basis detecta estas capacidades en runtime y transcodifica la misma codificación intermedia a la mejor coincidencia. Por eso esta capa de Basis es casi insustituible en la web — no puedes predecir el dispositivo del usuario antes de publicar.
Flujo de subida comparado: tradicional vs formatos de GPU
Finalmente, fija la diferencia en un diagrama de flujo.
PNG/JPG tradicional:
Archivo PNG ──descarga──> memoria CPU ──decode CPU (lento)──> bloque píxeles RGBA ──subida (grande, lenta)──> VRAM (87 MB)
KTX2 + Basis:
Archivo KTX2 ──descarga──> memoria CPU ──transcode runtime (rápido)──> formato de bloque GPU ──subida (pequeño, rápida)──> VRAM (11 MB)
Este último elimina el gran paso de «decodificación por píxel en CPU», y los datos subidos son un orden de magnitud menores. Primer frame más rápido, menos VRAM — ese es el valor central de este enfoque.
Siguiente paso
La teoría está lista; el próximo artículo es práctica. Comprimiremos texturas a KTX2 con toktx y gltf-transform, las cargaremos en Three.js / Babylon.js y trataremos cómo elegir ETC1S vs UASTC y cómo ajustar los parámetros de compresión.