Formatos comprimidos: BC, ETC2, ASTC y cuál pedir
Las tres familias de compresión de texturas, qué feature habilita cada una, el reparto real por plataforma, y la estrategia de supercompresión con Basis Universal.
Una textura de 2048×2048 en rgba8unorm ocupa 16 MiB en VRAM, y con mipmaps 21 MiB. Cien texturas así son 2 GiB, más de lo que tiene la mayoría de los dispositivos. Los formatos comprimidos reducen eso entre cuatro y ocho veces y, a diferencia de un JPEG, la GPU los descomprime en el momento de muestrear sin descomprimirlos en memoria. El problema es que no hay un formato universal, y la estrategia para vivir con eso es lo que separa un proyecto que funciona en todas partes de uno que no.
- Enumerar las tres familias de compresión, sus features y sus formatos.
- Calcular el tamaño en memoria de una textura comprimida a partir del bloque.
- Detectar qué familia soporta un dispositivo y elegir la variante correspondiente.
- Explicar qué es la supercompresión y por qué resuelve el problema de distribución.
Las tres familias
BC —Block Compression, también conocida como DXT o S3TC en sus primeras versiones— es la familia de escritorio. La habilita la feature texture-compression-bc y está en prácticamente todas las GPUs de PC y Mac. Sus formatos son bc1-rgba-unorm, bc1-rgba-unorm-srgb, bc2-rgba-unorm, bc2-rgba-unorm-srgb, bc3-rgba-unorm, bc3-rgba-unorm-srgb, bc4-r-unorm, bc4-r-snorm, bc5-rg-unorm, bc5-rg-snorm, bc6h-rgb-ufloat, bc6h-rgb-float, bc7-rgba-unorm y bc7-rgba-unorm-srgb.
ETC2 es la familia obligatoria de OpenGL ES 3.0, presente en el hardware Android. La habilita texture-compression-etc2, y sus formatos son etc2-rgb8unorm, etc2-rgb8unorm-srgb, etc2-rgb8a1unorm, etc2-rgb8a1unorm-srgb, etc2-rgba8unorm, etc2-rgba8unorm-srgb, eac-r11unorm, eac-r11snorm, eac-rg11unorm y eac-rg11snorm.
ASTC es la más flexible y la más moderna. La habilita texture-compression-astc, y está en todo el hardware Apple desde el A8 y en la mayoría de las GPUs móviles recientes. Sus formatos combinan catorce tamaños de bloque —4×4, 5×4, 5×5, 6×5, 6×6, 8×5, 8×6, 8×8, 10×5, 10×6, 10×8, 10×10, 12×10 y 12×12— con las variantes -unorm y -unorm-srgb, dando veintiocho formatos. Cada bloque ocupa siempre 16 bytes, así que el tamaño del bloque decide directamente la tasa de compresión: de 8 bits por texel con 4×4 hasta 0,89 con 12×12.
Hay además dos features para volúmenes: texture-compression-bc-sliced-3d y texture-compression-astc-sliced-3d, que permiten crear texturas comprimidas con dimension: '3d'. Sin ellas, los formatos comprimidos solo valen para '2d'.
Los tamaños de bloque y el cálculo de memoria
Cada formato tiene un tamaño de bloque en texels y un tamaño de bloque en bytes:
| Formato | Bloque | Bytes/bloque | Bits/texel | Qué guarda |
|---|---|---|---|---|
bc1-rgba-unorm |
4×4 | 8 | 4 | RGB más 1 bit de alfa |
bc3-rgba-unorm |
4×4 | 16 | 8 | RGB más alfa completo |
bc4-r-unorm |
4×4 | 8 | 4 | un canal |
bc5-rg-unorm |
4×4 | 16 | 8 | dos canales |
bc6h-rgb-float |
4×4 | 16 | 8 | RGB en HDR |
bc7-rgba-unorm |
4×4 | 16 | 8 | RGBA de alta calidad |
etc2-rgb8unorm |
4×4 | 8 | 4 | RGB |
etc2-rgba8unorm |
4×4 | 16 | 8 | RGBA |
astc-4x4-unorm |
4×4 | 16 | 8 | RGBA |
astc-6x6-unorm |
6×6 | 16 | 3,56 | RGBA |
astc-8x8-unorm |
8×8 | 16 | 2 | RGBA |
El cálculo de memoria de un nivel es directo:
function bytesDeNivel(ancho, alto, bloqueX, bloqueY, bytesPorBloque) {
return Math.ceil(ancho / bloqueX) * Math.ceil(alto / bloqueY) * bytesPorBloque;
}
// 2048×2048 en bc7: 512 × 512 × 16 = 4 MiB, frente a 16 MiB sin comprimir.
bytesDeNivel(2048, 2048, 4, 4, 16);
Los Math.ceil importan: una textura de 300×200 en un formato de bloque 4×4 usa 75×50 bloques, y la dimensión tiene que ser múltiplo del bloque para crear la textura. La validación de createTexture exige explícitamente que width sea múltiplo del ancho de bloque y height del alto de bloque.
Ese requisito hace que los niveles de mip más pequeños de una textura comprimida sean problemáticos: el nivel de 2×2 texels no cabe en un bloque de 4×4. La solución habitual de las herramientas es no generar los últimos niveles, o generarlos y aceptar que ocupan un bloque completo.
bc7 es el que mejor calidad da para color con alfa y es lo que se usa por defecto en escritorio. bc1 es la mitad de tamaño y suficiente para texturas de detalle sin alfa. bc4 es para mapas de un canal —rugosidad, oclusión, altura—, y bc5 para mapas de normales guardando solo X e Y y reconstruyendo Z en el shader, que además mejora la calidad porque dedica todos los bits a dos canales. bc6h es el único formato comprimido HDR y es lo correcto para mapas de entorno.
Detectar y elegir
La estrategia es pedir las tres features y elegir en tiempo de ejecución qué archivo cargar:
const adapter = await navigator.gpu.requestAdapter();
const compresion = ['texture-compression-astc',
'texture-compression-bc',
'texture-compression-etc2'].filter((f) => adapter.features.has(f));
const device = await adapter.requestDevice({ requiredFeatures: compresion });
function familiaPreferida(device) {
if (device.features.has('texture-compression-astc')) return 'astc';
if (device.features.has('texture-compression-bc')) return 'bc';
if (device.features.has('texture-compression-etc2')) return 'etc2';
return null; // sin compresión
}
const SUFIJOS = { astc: '.astc.ktx2', bc: '.bc7.ktx2', etc2: '.etc2.ktx2' };
const familia = familiaPreferida(device);
const url = familia ? `/tex/madera${SUFIJOS[familia]}` : '/tex/madera.png';
El reparto real en 2026 es este. Escritorio Windows y Linux: BC casi siempre, ASTC en algunas GPUs recientes. macOS con Apple Silicon: ASTC y BC. iOS y iPadOS: ASTC. Android: ETC2 garantizado, ASTC en la mayoría del hardware desde 2016. En la práctica, ofrecer variantes ASTC y BC cubre casi todo, y ETC2 es la red de seguridad para Android antiguo.
Almacenar tres variantes de cada textura triplica el peso del despliegue, que es el motivo por el que existe la siguiente sección.
La supercompresión
Un archivo comprimido con BC7 no se comprime bien con gzip ni con Brotli: sus bytes ya son casi aleatorios. Y guardar tres variantes triplica el ancho de banda de descarga. La solución del ecosistema es Basis Universal: un formato intermedio que se transcodifica en el cliente al formato nativo del dispositivo.
El flujo tiene tres pasos. En el pipeline de assets, la textura se codifica una vez a .ktx2 con supercompresión Basis, en modo ETC1S —muy compacto, calidad media— o UASTC —mayor calidad, más peso—. Ese archivo se sirve comprimido con Brotli, que sobre ETC1S sí funciona bien. Y en el cliente, un transcodificador en WebAssembly convierte los datos al formato que el dispositivo soporta, en milisegundos y sin pérdida adicional respecto a codificar directamente.
El resultado es un solo archivo por textura, más pequeño que cualquiera de las variantes nativas, que funciona en todos los dispositivos. El coste es el transcodificador —unos cientos de kilobytes de WebAssembly, cargados una vez— y unos milisegundos de CPU por textura, que conviene mover a un worker.
Una vez transcodificados, los datos se suben nivel a nivel con writeTexture, porque el archivo ya trae la cadena de mips completa:
// datos: array con un Uint8Array por nivel, ya transcodificado
const textura = device.createTexture({
size: [ancho, alto, 1],
format: 'bc7-rgba-unorm-srgb',
mipLevelCount: datos.length,
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
});
for (let nivel = 0; nivel < datos.length; nivel++) {
const w = Math.max(1, ancho >> nivel);
const h = Math.max(1, alto >> nivel);
device.queue.writeTexture(
{ texture: textura, mipLevel: nivel },
datos[nivel],
{ bytesPerRow: Math.ceil(w / 4) * 16, rowsPerImage: Math.ceil(h / 4) },
[w, h, 1],
);
}
La cuenta que todo el mundo hace es la de VRAM: cuatro veces menos memoria, cuatro veces más texturas. La cuenta que casi nadie hace es la del ancho de banda de muestreo, y es la que de verdad se nota. Cuando un fragment shader muestrea una textura, la GPU trae del bus la línea de caché que contiene el texel. Con rgba8unorm, esa línea de 128 bytes contiene 32 texels. Con bc7, contiene 8 bloques de 4×4, o sea 128 texels: cuatro veces más cobertura por el mismo tráfico, y por tanto cuatro veces menos fallos de caché en un patrón de acceso espacialmente coherente, que es lo que produce el rasterizado. En un shader con seis muestras por fragmento y una escena limitada por ancho de banda, pasar de sin comprimir a BC7 no mejora un 5 %: puede duplicar el frame rate. Es la razón por la que los formatos comprimidos siguen usándose en escritorios con 24 GiB de VRAM, donde el ahorro de memoria es irrelevante. La descompresión, por cierto, es gratis: la hace hardware fijo dentro de la unidad de texturas, no el shader.