wandres.dev
TEXTURAS I · Crear, subir y muestrear

La tabla de formatos: renderizable, filtrable, storage

Qué puede hacer cada formato de textura de WebGPU, las tres capacidades que no son adivinables, los formatos de profundidad, y las features que amplían la tabla.

⏱ 20 min

El formato de una textura no solo dice cuántos bits ocupa cada canal: dice si se puede filtrar, si se puede renderizar a ella, si se puede mezclar con blending y si un compute shader puede escribirla. Esas cuatro capacidades no se deducen del nombre y no son las mismas para formatos que parecen equivalentes. rgba8unorm se puede usar como storage texture; rgba8unorm-srgb no. Saber la tabla ahorra una clase entera de errores de validación.

🎯 Al terminar esta lección sabrás
  • Leer la tabla de capacidades de los formatos de color más usados.
  • Distinguir filtrable de renderizable de mezclable de storage.
  • Elegir el formato de profundidad correcto según lo que necesites del stencil.
  • Saber qué features amplían la tabla y cuándo merece la pena pedirlas.

Las cuatro capacidades

Filtrable significa que un sampler con magFilter o minFilter en 'linear' puede interpolar sus texels. Los formatos enteros nunca lo son —no tiene sentido interpolar índices— y los de 32 bits en coma flotante tampoco lo son en el núcleo de la especificación.

Renderizable significa que la textura puede ser un attachment de color de un render pass, con RENDER_ATTACHMENT en su usage.

Mezclable es una capacidad adicional dentro de renderizable: permite además usar blend en el target del pipeline. Todos los formatos mezclables son renderizables; no al revés.

Storage significa que admite STORAGE_BINDING y por tanto puede ser el destino de un textureStore() desde un shader.

Los formatos de color más usados

Formato Bytes Tipo de muestra Renderizable Mezclable Storage
r8unorm 1 float no
r8uint / r8sint 1 uint / sint no no
rg8unorm 2 float no
r16float 2 float no
r16uint / r16sint 2 uint / sint no no
rgba8unorm 4 float
rgba8unorm-srgb 4 float no
rgba8snorm 4 float no no
rgba8uint / rgba8sint 4 uint / sint no
bgra8unorm 4 float solo con feature
bgra8unorm-srgb 4 float no
rg16float 4 float no
r32float 4 unfilterable-float solo con feature
r32uint / r32sint 4 uint / sint no
rgb10a2unorm 4 float no
rgb10a2uint 4 uint no no
rg11b10ufloat 4 float solo con feature solo con feature no
rgb9e5ufloat 4 float no no no
rgba16float 8 float
rgba16uint / rgba16sint 8 uint / sint no
rg32float 8 unfilterable-float solo con feature
rg32uint / rg32sint 8 uint / sint no
rgba32float 16 unfilterable-float solo con feature
rgba32uint / rgba32sint 16 uint / sint no

Cuatro patrones se leen en la tabla y merecen quedarse.

Los formatos -srgb no son storage. La conversión sRGB es una operación del hardware de muestreo y de mezcla, no del camino de escritura de un compute shader. Si necesitas escribir sRGB desde un compute, escribes en rgba8unorm y muestreas con una vista -srgb gracias a viewFormats.

Los formatos de 32 bits en coma flotante no son filtrables por defecto. r32float, rg32float y rgba32float tienen sampleType: 'unfilterable-float', lo que obliga a usar un sampler 'non-filtering' con magFilter y minFilter en 'nearest'. La feature float32-filterable lo cambia. Para HDR intermedio, rgba16float es filtrable de serie y ocupa la mitad: casi siempre es la elección correcta.

rgba8snorm no es renderizable. Es filtrable y es storage, pero no puede ser attachment de un render pass en el núcleo de la especificación. La feature texture-formats-tier1 lo cambia.

bgra8unorm es especial porque es el formato del canvas. navigator.gpu.getPreferredCanvasFormat() devuelve 'bgra8unorm' en la mayoría de las plataformas de escritorio y 'rgba8unorm' en algunas móviles. Escribir a él desde un compute shader requiere la feature bgra8unorm-storage.

Los formatos de profundidad y stencil

Formato Aspecto Precisión Notas
depth16unorm profundidad 16 bits normalizados el más barato, precisión justa
depth24plus profundidad al menos 24 bits la implementación elige
depth24plus-stencil8 ambos al menos 24 más 8 el uso general
depth32float profundidad 32 bits float el preciso
depth32float-stencil8 ambos 32 más 8 requiere feature
stencil8 stencil 8 bits solo stencil

depth24plus no garantiza exactamente 24 bits: garantiza al menos 24, y la implementación puede usar 32 float internamente. Eso significa que no puedes hacer suposiciones sobre el patrón de bits, y de hecho depth24plus y depth24plus-stencil8 no admiten copias hacia o desde buffers de su aspecto de profundidad. Si necesitas leer el depth buffer de vuelta a CPU, tiene que ser depth32float.

Los formatos de profundidad tienen sampleType: 'depth', y en el shader se declaran como texture_depth_2d, no como texture_2d<f32>. Se muestrean con textureSampleCompare usando un sampler_comparison, o se leen con textureLoad.

depth32float-stencil8 requiere la feature del mismo nombre y no está en todas las implementaciones.

💡
Elegir el formato de profundidad

Si solo necesitas el test de profundidad, depth24plus con TRANSIENT_ATTACHMENT es lo más barato. Si además usas stencil —máscaras, portales, contornos—, depth24plus-stencil8. Si necesitas leer la profundidad en un post-proceso con precisión, depth32float. Y si vas a hacer reversed-Z, depth32float es casi obligatorio: es donde la distribución de precisión del float compensa la de la proyección perspectiva.

Las features que amplían la tabla

Cinco features tocan directamente estas capacidades, y todas se piden en requestDevice después de comprobarlas en adapter.features.

float32-filterable hace filtrables r32float, rg32float y rgba32float.

float32-blendable permite blending sobre esos mismos tres formatos.

bgra8unorm-storage permite STORAGE_BINDING sobre bgra8unorm, que es lo que hace falta para escribir directamente al canvas desde un compute shader.

rg11b10ufloat-renderable hace rg11b10ufloat renderizable y mezclable. Es un formato de 4 bytes sin alfa con rango HDR, atractivo como buffer intermedio de iluminación por ocupar la mitad que rgba16float.

texture-formats-tier1 es un paquete grande. Añade RENDER_ATTACHMENT —con mezcla y multisampling— a r8snorm, rg8snorm y rgba8snorm; añade STORAGE_BINDING con acceso de solo lectura y solo escritura a una lista larga de formatos de 8 y 16 bits (r8unorm, r8snorm, r8uint, r8sint, rg8unorm, rg8snorm, rg8uint, rg8sint, r16uint, r16sint, r16float, rg16uint, rg16sint, rg16float, rgb10a2uint, rgb10a2unorm, rg11b10ufloat); y habilita los formatos r16unorm, r16snorm, rg16unorm, rg16snorm, rgba16unorm y rgba16snorm. Habilitarla implica rg11b10ufloat-renderable.

texture-formats-tier2 amplía el acceso read-write de las storage textures a r8unorm, r8uint, r8sint, rgba8unorm, rgba8uint, rgba8sint, r16uint, r16sint, r16float, rgba16uint, rgba16sint, rgba16float, rgba32uint, rgba32sint y rgba32float. Implica las dos anteriores.

const deseadas = ['float32-filterable', 'rg11b10ufloat-renderable',
                  'texture-formats-tier1'];
const device = await adapter.requestDevice({
  requiredFeatures: deseadas.filter((f) => adapter.features.has(f)),
});
// Después, el código consulta device.features y elige la ruta correspondiente.
El formato que eliges es una decisión de ancho de banda, no de precisión

La forma habitual de elegir formato es preguntarse cuánta precisión hace falta, y suele llevar a elegir de más. La pregunta útil es la contraria: cuánto ancho de banda cuesta. Un G-buffer de tres attachments a rgba32float en 4K son 1920 MiB por frame de tráfico de escritura más lo mismo de lectura, que a 60 fps son 230 GB por segundo: por encima del ancho de banda de casi cualquier GPU integrada y de bastantes discretas. Los mismos tres attachments en rgba16float bajan a la mitad, y empaquetando las normales en rg16float con reconstrucción de la tercera componente y el material en rgba8unorm, a la cuarta parte. La diferencia visual entre esas configuraciones es casi nula; la diferencia de frame rate es del doble. En un pipeline gráfico moderno, el formato de las texturas intermedias es la palanca de rendimiento más grande que existe, y se acciona cambiando una cadena de texto.