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.
Un JPEG de 200 KB y un PNG de 4 MB de la misma imagen ocupan exactamente lo mismo en la memoria de vídeo: ancho por alto por cuatro bytes. La compresión de formatos de imagen es de transporte, no de almacenamiento en GPU: el navegador descomprime a píxeles y sube píxeles. Esa asimetría es la que explica por qué una escena que descarga cinco megabytes puede consumir seiscientos de VRAM, y por qué KTX2 es cualitativamente distinto de cualquier optimización de formato de imagen.
- Calcular la memoria de vídeo que consume una textura y comprobar que no depende de su formato de archivo.
- Explicar qué es la compresión por bloques y por qué la GPU puede muestrear de ella directamente.
- Distinguir ETC1S de UASTC y elegir uno según el tipo de mapa.
- Estimar el ahorro real de VRAM de convertir una escena a KTX2.
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.
La compresión por bloques trabaja en bloques de 4 por 4. Una textura de 1023 píxeles de ancho no se puede comprimir sin rellenar. Las herramientas lo hacen por ti, pero el relleno desperdicia espacio y puede introducir artefactos en el borde. Trabaja siempre con potencias de dos, que además son las que permiten mipmaps completos.
Convertir
Con gltf-transform, sobre un modelo entero:
# Color y ORM en ETC1S, con calidad alta del libro de codigos
gltf-transform etc1s modelo.glb salida.glb \
--slots "!normalTexture" --quality 200
# Las normales aparte, en UASTC con Zstd
gltf-transform uastc salida.glb final.glb \
--slots "normalTexture" --level 4 --zstd 18
El 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.png
toktx --encode uastc --uastc_quality 3 --zcmp 18 --genmipmap normal.ktx2 normal.png
Es fácil ver KTX2 como la solución al problema de las texturas y saltarse el paso anterior, que es más aburrido y da más ahorro. La compresión por bloques tiene un ratio fijo: entre 4 y 8 a 1 según el formato, y no depende del contenido. Reducir la resolución a la mitad, en cambio, divide la memoria por cuatro, y se puede aplicar varias veces. Una textura de 4K sin comprimir son 89 MB; en ASTC son 22 MB; a 2K sin comprimir son 22 MB también; y a 2K en ASTC son 5.6 MB. Es decir: bajar de 4K a 2K ahorra exactamente lo mismo que aplicar KTX2, y hacer las dos cosas multiplica los ahorros. La pregunta que hay que hacerse antes de comprimir nada es cuántos píxeles de pantalla ocupa esa textura en el peor caso realista. Si un objeto nunca ocupa más de 400 píxeles de alto en la vista más cercana que permite tu control de cámara, una textura de 4K le está dando diez veces más detalle del que se puede ver: los mipmaps la reducirán a 512 antes de dibujarla, y habrás descargado y subido a la GPU 89 megabytes para usar 1.4. Esto ocurre constantemente porque los modelos de las bibliotecas vienen a 4K por defecto, ya que sus autores no saben a qué distancia los vas a mirar. gltf-transform resize --width 1024 --height 1024 antes de comprimir es el comando de mejor relación entre esfuerzo y ahorro de todo el pipeline, y en un modelo descargado de internet suele quitar más peso que Draco y KTX2 juntos. La disciplina correcta es: primero decide la resolución por la distancia de visionado, después comprime. Al revés, estás comprimiendo píxeles que nadie va a ver.