KTX2 y Basis Universal: el ahorro que se nota en VRAM
Por qué un JPEG ocupa lo mismo que un PNG en la memoria de vídeo, qué es la compresión por bloques, la diferencia entre ETC1S y UASTC, y cómo elegir entre las dos.
El cálculo que casi nadie hace
La memoria que ocupa una textura en la GPU es:
ancho * alto * bytes por texel * ( 1 + 1/3 si hay mipmaps )Para RGBA de 8 bits por canal, cuatro bytes por texel. Una textura de 2048 por 2048 con mipmaps ocupa 22.4 MB. Un material PBR completo tiene cinco: color, normal, ORM, emisión y a veces desplazamiento. Son 112 MB por material.
| Textura | Peso en disco | VRAM sin comprimir | VRAM en ASTC 4×4 |
|---|---|---|---|
| 1024² JPEG | 180 KB | 5.6 MB | 1.4 MB |
| 2048² JPEG | 600 KB | 22.4 MB | 5.6 MB |
| 2048² PNG | 6 MB | 22.4 MB | 5.6 MB |
| 4096² JPEG | 2 MB | 89.5 MB | 22.4 MB |
Las dos filas del medio son la demostración: el mismo contenido, treinta veces más peso en disco, la misma VRAM exacta. Optimizar el formato de imagen mejora el tiempo de descarga y no toca el problema de memoria.
En un móvil con un presupuesto realista de 300 a 500 MB de memoria de gráficos, tres materiales PBR completos a 2K ya lo agotan. Ese es el techo que KTX2 rompe.
Compresión por bloques
Las GPUs soportan nativamente formatos donde la textura permanece comprimida en memoria y la unidad de muestreo descomprime sobre la marcha. Todos funcionan igual: la imagen se divide en bloques de 4 por 4 texels y cada bloque se codifica con un número fijo de bits.
En BC1, por ejemplo, cada bloque de 16 texels se guarda en 8 bytes: dos colores extremos de 16 bits y dos bits por texel para interpolar entre ellos. Ratio fijo de 8 a 1 respecto a RGBA.
Las tres propiedades que hacen esto viable, y que un JPEG no tiene:
Tasa de bits fija. Cada bloque ocupa lo mismo, así que la dirección de cualquier texel se calcula con aritmética. En un JPEG, la posición de un píxel depende de todo lo anterior; sería imposible acceder aleatoriamente.
Descompresión en hardware, sin coste. La unidad de texturas lo hace en su ruta fija. No consume ni una instrucción de shader.
Menos ancho de banda. Como se lee comprimido, cada acceso mueve cuatro u ocho veces menos bytes. En una GPU limitada por memoria —casi todas las de móvil— eso es una ganancia de rendimiento además de una de espacio.
El problema histórico es que cada familia de GPU tiene sus formatos: BC en escritorio, ETC2 en Android, ASTC en móviles modernos, PVRTC en iOS antiguo. Servir todas las variantes multiplica el almacenamiento y complica el despliegue.
Basis Universal: un fichero, todas las GPUs
Basis resuelve eso con un formato intermedio transcodificable. El fichero guarda una representación que se convierte al formato nativo de la GPU concreta en el cliente, en milisegundos, sin descomprimir a píxeles. Un fichero, todos los dispositivos.
KTX2 es el contenedor estándar de Khronos que envuelve esos datos, y KHR_texture_basisu es la extensión de glTF que lo referencia.
Hay dos modos y elegir mal es el error más común:
ETC1S. El modo de alta compresión. Usa un libro de códigos global compartido entre todas las texturas del fichero, con lo que consigue ratios enormes. Muy pequeño en disco, transcodifica rápido, y pierde calidad de forma visible en gradientes suaves y en mapas de normales. Está pensado para color y para texturas con detalle.
UASTC. El modo de alta calidad. Cada bloque se codifica de forma independiente con 128 bits, casi sin pérdida perceptible. Pesa bastante más en disco —se suele acompañar de compresión Zstandard supercomprimida, que el contenedor KTX2 soporta— y da la misma VRAM que ETC1S, porque ambos transcodifican a formatos de bloque de tamaño similar.
| ETC1S | UASTC | |
|---|---|---|
| Bits por texel en disco | ~0.5 a 1 | 8, o ~2 con Zstd |
| Calidad | media, artefactos en degradados | alta, casi sin pérdida |
| Apto para mapas de normales | no | sí |
| Apto para color y ORM | sí | sí, si sobra presupuesto |
| Transcodificación | rápida | rápida |
La regla que se sigue en producción: ETC1S para color base y para ORM, UASTC para normales. Un mapa de normales en ETC1S produce un sombreado con bandas y facetas que se ve inmediatamente en superficies suaves, porque los tres canales del normal son gradientes puros y es exactamente donde ETC1S falla.
El ahorro real
Convertir una escena de producto típica —cinco materiales, veinte texturas de 2K— da estos números:
| Antes, JPEG/PNG | Después, KTX2 | |
|---|---|---|
| Descarga de texturas | 14 MB | 4.2 MB |
| VRAM de texturas | 448 MB | 112 MB |
| Tiempo de subida a GPU | ~700 ms | ~180 ms |
| Transcodificación extra | 0 | ~90 ms, en workers |
La fila de VRAM es la que cambia lo que se puede hacer: una escena que antes solo funcionaba en escritorio pasa a funcionar en un móvil de gama media. Y la de tiempo de subida también cuenta: subir 112 MB a la GPU es cuatro veces más rápido que subir 448.
Convertir
Con gltf-transform, sobre un modelo entero:
# Color y ORM en ETC1S, con calidad alta del libro de codigosgltf-transform etc1s modelo.glb salida.glb \ --slots "!normalTexture" --quality 200# Las normales aparte, en UASTC con Zstdgltf-transform uastc salida.glb final.glb \ --slots "normalTexture" --level 4 --zstd 18El selector --slots acepta el nombre del slot de textura de glTF y admite negación con !. Es lo que permite aplicar dos modos distintos en dos pasadas sobre el mismo fichero.
Con toktx, la herramienta oficial de Khronos, para texturas sueltas:
toktx --encode etc1s --clevel 4 --qlevel 200 --genmipmap color.ktx2 color.pngtoktx --encode uastc --uastc_quality 3 --zcmp 18 --genmipmap normal.ktx2 normal.png