Pular para o conteúdo
Fundamento · CoordenadasIntermediário

Derivadas em espaço de tela

dFdx, dFdy e fwidth medem o quanto um valor muda de um pixel para o seguinte, para bordas que ficam com um pixel de suavidade em qualquer tamanho.

Como funciona

Um shader de fragmento parece enxergar um pixel por vez, mas não é assim que a GPU o executa. Ela sombreia os pixels em blocos de 2×2 chamados quads, os quatro rodando as mesmas instruções ao mesmo tempo. Isso permite responder uma pergunta que nenhum pixel sozinho responderia: o quanto esse valor muda entre mim e o meu vizinho? A resposta é uma derivada em espaço de tela.

dFdx, dFdy e fwidth

  • dFdx(v) é, aproximadamente, a diferença de v entre este pixel e o vizinho horizontal no quad.
  • dFdy(v) é o mesmo, na vertical.
  • fwidth(v) é abs(dFdx(v)) + abs(dFdy(v)): o quanto v muda em cerca de um pixel, em qualquer direção.

Elas funcionam com qualquer expressão float ou vetorial que o shader calcula, não só com as entradas. Se uv vai de 0 a 1 num canvas de 1000 pixels, fwidth(uv.x) vale cerca de 0.001. Aproxime a câmera da superfície e o valor diminui; incline a superfície para longe, espremendo a textura em menos pixels, e ele aumenta. O GLSL ES 3.00 tem as três embutidas (o WebGL 1 precisava de uma extensão), e elas só existem no shader de fragmento.

Bordas de um pixel

Um smoothstep com largura fixa, como smoothstep(0.49, 0.51, d), fica nítido num tamanho e borrado ou serrilhado em outro, porque 0.02 unidade cobre um número diferente de pixels a cada vez. O fwidth mede um pixel nas unidades de d, então a borda pode ter exatamente essa largura onde quer que seja desenhada:

float d = length(p) - 0.5;             // distância com sinal até um círculo de raio 0.5
float w = fwidth(d);                   // um pixel, nas unidades de d
float disc = clamp(0.5 - d / w, 0.0, 1.0);

A última linha é uma rampa linear de 1 a 0 ao longo de um pixel centrado na borda, perto de quanto do pixel a forma cobre. Isso é antialiasing analítico: nenhuma amostra extra, só uma derivada. Funciona com qualquer campo que mude de forma suave, e é por isso que campos de distância com sinal e derivadas andam juntos.

Filtrando um padrão

Um padrão repetido desenhado com fract sofre aliasing à distância: quando um período fica menor que um pixel, o pixel pega um ponto praticamente aleatório dentro dele, e o padrão se desfaz em cintilação e moiré. A derivada diz quantos pixels um período cobre, então o padrão pode sumir antes que isso aconteça. Aqui, linhas de grade que se apagam ao longo de um pixel de cada lado, a qualquer distância:

vec2 g = uv * 10.0;                              // dez células por unidade
vec2 fw = fwidth(g);                             // células por pixel, em cada eixo
vec2 dist = abs(fract(g - 0.5) - 0.5) / fw;      // distância até a linha mais próxima, em pixels
float line = 1.0 - clamp(min(dist.x, dist.y), 0.0, 1.0);

Quando fw passa de 0.5, as linhas preenchem a célula inteira. Uma solução completa leva line em direção ao valor médio do padrão conforme fw cresce. A filtragem de texturas faz a mesma coisa em hardware: o nível de mipmap que uma consulta de textura escolhe vem das derivadas das coordenadas dela.

Uma normal de graça

As derivadas também dão a orientação de uma superfície. As diferenças de posição no mundo para a direita e para cima são dois vetores ao longo da superfície; o produto vetorial deles é perpendicular a ela:

vec3 n = normalize(cross(dFdx(vWorldPos), dFdy(vWorldPos)));

O resultado é constante em cada triângulo, um visual facetado, e não precisa de normais na malha. Inverta o sinal se as faces saírem escuras: ele depende da convenção do eixo y.

Armadilhas

  • Desvios. As derivadas comparam valores dentro do quad, então os quatro pixels precisam calcular o valor. Dentro de um if cuja condição muda entre vizinhos, o resultado é indefinido. Calcule as derivadas, e as consultas de textura que dependem delas, antes do desvio.
  • Saltos. Um valor com uma descontinuidade, como fract(x) na virada ou um ângulo de atan em ±π, tem uma derivada enorme ali, e um filtro baseado nela desenha uma emenda. Tire a derivada do valor suave antes do salto, como a grade acima faz com g em vez de fract(g). Para um ângulo polar t, que não tem versão suave, tire as derivadas de t e de fract(t + 0.5), fique com o par de menor fwidth e passe-o ao textureGrad.
  • Precisão. As derivadas são aproximadas: muitas vezes um único valor vale para o quad inteiro. Servem para filtragem e larguras de borda, não para contas exatas.

Leitura complementar