Pular para o conteúdo
Fundamento · AmostragemIntermediário

Filtragem e mipmaps

Como a GPU mistura texels quando uma textura aparece maior ou menor que o próprio tamanho, e por que texturas distantes cintilam sem mipmaps.

Como funciona

Os pixels da tela e os texels de uma textura quase nunca se alinham um para um. De perto, um texel cobre muitos pixels; de longe, um pixel cobre muitos texels. Filtragem é o que a GPU faz nos dois casos, e ela é configurada na textura ou no sampler pelo código do host. O shader só vê o resultado, mas é o resultado que decide se a imagem fica nítida, suave, quadriculada ou cheia de cintilação.

Ampliação: nearest ou linear

Quando uma textura aparece maior que o próprio tamanho, cada pixel cai em algum lugar entre os centros dos texels.

  • Nearest devolve o texel mais próximo. As bordas continuam duras e os texels viram quadrados visíveis: a escolha certa para pixel art e para dados que não podem ser misturados, como índices de uma paleta.
  • Linear (bilinear) mistura os quatro texels mais próximos, com pesos pela proximidade da amostra a cada centro. A imagem fica suave em vez de quadriculada. É a escolha padrão para fotos e para a maioria das superfícies.

A filtragem bilinear também é uma mistura de graça: uma amostra posta exatamente entre dois texels devolve a média deles pelo preço de uma leitura. Kernels de desfoque usam isso para ler metade das vezes.

Redução e aliasing

Quando uma textura aparece menor que o próprio tamanho, a filtragem bilinear continua lendo só quatro texels por pixel e pula tudo o que fica entre eles. Quais texels são lidos muda quando a superfície anda uma fração de pixel, então os detalhes finos viram um ruído que rasteja e cintila, e padrões regulares como tijolos ou listras produzem moiré. Isso é aliasing: a textura tem mais detalhe do que os pixels conseguem mostrar, e as amostras pontuais pegam esse detalhe ao acaso.

Mipmaps

Uma cadeia de mipmaps resolve a redução fazendo a média de antemão. O nível 0 é a própria textura, o nível 1 tem metade da largura e da altura, o nível 2 um quarto, e assim por diante até um texel. Cada nível tira a média de blocos do nível de cima, então ler um nível pequeno devolve a média de tudo o que um pixel distante cobre. A cadeia inteira acrescenta um terço à memória da textura.

A GPU escolhe um nível por pixel a partir de quantos texels o pixel cobre. Ela mede isso comparando a coordenada de textura dentro de cada bloco de 2×2 pixels que sombreia junto: se a coordenada anda 4 texels de um pixel para o próximo, o nível 2 (4 = 2²) tem mais ou menos um texel por pixel. O nível é o log na base 2 desse passo:

// no escopo do arquivo:
uniform sampler2D uImage;
// dentro de main(): mais ou menos o nível que a GPU escolheria
vec2 texCoord = uv * vec2(textureSize(uImage, 0)); // em texels
vec2 dx = dFdx(texCoord);                          // variação até o próximo pixel à direita
vec2 dy = dFdy(texCoord);                          // variação até o próximo pixel acima
float level = 0.5 * log2(max(dot(dx, dx), dot(dy, dy)));

Os modos que usam mipmaps diferem no jeito de tratar os níveis:

  • Mipmap nearest (por exemplo LINEAR_MIPMAP_NEAREST) lê um nível só, bilinear dentro dele. É barato, mas a troca de nível pode aparecer como uma linha visível atravessando um piso.
  • Trilinear (LINEAR_MIPMAP_LINEAR) lê os dois níveis mais próximos de forma bilinear e mistura os dois pela parte fracionária do nível. Custa duas leituras bilineares e esconde a troca.

No WebGL o filtro de redução de uma textura começa como NEAREST_MIPMAP_LINEAR. Até você chamar gl.generateMipmap ou escolher um filtro sem mipmaps, a textura fica incompleta e toda leitura devolve preto. Texturas em que você renderiza também não têm mipmaps, a não ser que você os gere depois de cada renderização.

Filtragem anisotrópica

Num piso visto em ângulo rasante, a área coberta por um pixel é uma faixa longa e fina: cobre muitos texels em profundidade e poucos na largura. O nível é escolhido pelo lado mais longo, então a textura fica borrada também na largura. A filtragem anisotrópica tira várias amostras ao longo do lado comprido, num nível mais nítido, o que mantém nítidas as superfícies rasantes. É uma configuração do sampler; no WebGL2 ela vem da extensão EXT_texture_filter_anisotropic, com no máximo 16 amostras na maioria do hardware.

Assumindo o controle no shader

Três chamadas do GLSL substituem a escolha automática:

  • texture(sampler, uv, bias) soma bias ao nível: negativo fica mais nítido (e com mais aliasing), positivo fica mais suave.
  • textureLod(sampler, uv, level) lê um nível exato. Ela não precisa dos pixels vizinhos, então é segura dentro de desvios e laços, e é assim que cadeias de desfoque leem um nível reduzido específico.
  • textureGrad(sampler, uv, dPdx, dPdy) usa a taxa de variação que você passa em vez de medi-la.

O textureGrad corrige a emenda que ladrilhar com fract() deixa. Em cada borda de ladrilho a coordenada repetida salta de 1 para 0, a variação medida parece enorme e a GPU lê o menor nível numa fileira de pixels. Meça a variação na coordenada antes de repeti-la:

vec2 tiled = uv * 4.0;
vec4 c = textureGrad(uImage, fract(tiled), dFdx(tiled), dFdy(tiled)); // sem emenda

Padrões feitos só de matemática não têm mipmaps, então uma listra procedural fina sofre aliasing igualzinho a uma textura sem filtragem. As mesmas derivadas que escolhem o nível de mipmap deixam um padrão se borrar na largura de um pixel, assunto da página de derivadas.

Leitura complementar