Pular para o conteúdo
Fundamento · AmostragemIniciante

Amostragem de texturas

Leia uma imagem num shader com texture(), escolha como ela se repete nas bordas e faça ela rolar, ladrilhar ou virar páginas.

Como funciona

Uma textura é uma imagem que a GPU consegue ler de dentro de um shader: uma grade de texels, cada um com até quatro valores. Pode ser uma foto ou um sprite, mas também dados: um padrão de ruído, uma máscara, uma faixa de gradiente, a profundidade do último quadro. Ler uma textura se chama amostrar, e é o principal jeito de um shader receber informação que não sai de uma conta.

texture()

No GLSL ES 3.00 uma textura chega como um uniform sampler2D, e texture(sampler, coord) faz a leitura. A coordenada é normalizada: vai de 0 a 1 na imagem inteira, nos dois eixos, seja qual for o tamanho dela em pixels. É o mesmo intervalo de uma UV, então a superfície texturizada mais simples é uma linha só:

// no escopo do arquivo:
uniform sampler2D uImage; // ligado a uma unidade de textura pelo código do host
// dentro de main():
vec4 color = texture(uImage, uv); // rgba; uma imagem de 8 bits dá 0..1 por canal

O resultado é sempre um vec4. Uma textura de um canal só devolve o valor em r, com g e b em 0 e a em 1, então uma máscara guardada no canal vermelho é texture(uMask, uv).r. Os cards deste site declaram um sampler por textura, chamado t_<nome>.

Os centros dos texels ficam em posições de meio texel: numa imagem de 256 texels de largura, o centro da primeira coluna está em 0.5 / 256.0. textureSize(sampler, 0) devolve o tamanho em texels como um ivec2, o que dá o passo de um texel para o seguinte:

vec2 texel = 1.0 / vec2(textureSize(uImage, 0)); // um texel, em unidades de UV
vec4 right = texture(uImage, uv + vec2(texel.x, 0.0)); // o vizinho da direita

Quando você quer um texel exato, sem mistura nenhuma, texelFetch(sampler, ivec2(x, y), 0) recebe coordenadas inteiras de texel, ignora a repetição e a filtragem e devolve valores indefinidos fora da imagem. É a ferramenta certa para tabelas de consulta e texturas de dados.

Qual lado é para cima

A coordenada de textura (0, 0) é a primeira linha de texels na memória. Imagens na web guardam primeiro a linha de cima, então uma textura WebGL enviada sem cuidado aparece de cabeça para baixo sob uma UV com a origem embaixo à esquerda. Ou você inverte no envio (gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)), ou inverte no shader com vec2(uv.x, 1.0 - uv.y). As engines cuidam disso por você; é no WebGL escrito à mão que isso morde.

Modos de repetição

Uma coordenada fora de 0–1 é permitida. O que ela lê depende do modo de repetição (wrap) da textura, definido na textura ou no sampler pelo código do host, não no GLSL:

  • Repeat ladrilha a imagem: 1.25 lê o mesmo que 0.25.
  • Clamp to edge repete para sempre os texels da borda: tudo depois de 1 lê a última coluna.
  • Mirrored repeat ladrilha espelhando uma cópia sim, outra não, o que esconde a emenda de uma imagem que não foi feita para ladrilhar.

Texturas que ladrilham usam repeat; sprites, cópias da tela e faixas de consulta usam clamp to edge, senão as bordas pegam texels do lado oposto ao serem filtradas. O WebGL2 aceita repeat em texturas de qualquer tamanho, ao contrário do WebGL 1, que exigia potências de dois.

Ladrilhar, deslocar e rolar

Com o modo repeat, basta mover e escalar a coordenada:

vec2 tiled    = uv * 4.0;                     // quatro cópias na horizontal, quatro na vertical
vec2 offset   = tiled + vec2(0.3, 0.0);       // deslocada em 0.3 de cópia
vec2 scrolled = tiled + vec2(0.1, 0.0) * time; // anda 0.1 cópia por segundo
vec4 c = texture(uImage, scrolled);

Multiplicar a coordenada deixa a imagem menor na tela, porque cada cópia cobre menos da superfície. Prefira o modo de repetição ao fract() para ladrilhar: o fract salta de 1 para 0 entre as cópias, e a GPU enxerga esse salto quando escolhe o nível de mipmap, o que aparece como uma linha fina em cada emenda (a página de filtragem e mipmaps explica o porquê).

Flipbooks

Um flipbook guarda os quadros de uma animação numa textura só, numa grade de colunas e linhas. Cada quadro mostra uma célula: divida a UV pelo tamanho da grade e some o canto da célula.

const vec2 grid = vec2(4.0, 4.0);                // 4 colunas, 4 linhas: 16 quadros
float frame = mod(floor(time * 12.0), 16.0);      // 12 quadros por segundo, em loop
vec2 cell = vec2(mod(frame, grid.x), grid.y - 1.0 - floor(frame / grid.x));
vec4 c = texture(uImage, (cell + uv) / grid);

O grid.y - 1.0 - coloca o quadro 0 na linha de cima, onde as folhas de sprites costumam começar, quando a origem da textura está embaixo à esquerda. Deixe alguns texels de margem em volta de cada quadro na folha: a filtragem e os mipmaps leem um pouco além da borda da célula, e sem margem o quadro vizinho vaza para dentro.

Coordenadas calculadas

A coordenada pode ser qualquer expressão: uma UV rolando, uma UV curvada por uma senoide ou um valor lido de outra textura. Uma leitura cuja coordenada depende de uma leitura anterior se chama leitura dependente. No GLSL ES 3.00 ela é permitida, mas cada uma espera a busca anterior terminar, então cadeias longas custam tempo. Um cuidado: dentro de um if ou de um laço cuja condição muda de pixel para pixel, texture() pode devolver valores errados nas bordas do desvio, porque a GPU escolhe o nível de mipmap comparando pixels vizinhos e alguns deles não executaram a chamada. Amostre antes do desvio, ou use ali textureLod com um nível explícito.

Leitura complementar