Um fragment shader parece rodar sozinho, um pixel por vez, sem saber nada dos vizinhos. Não é assim. A GPU roda os pixels em grupos de quatro, e um shader pode perguntar quanto qualquer valor muda de um pixel para o seguinte. Essas respostas, as derivadas dFdx, dFdy e fwidth, são o que deixa as bordas suaves em qualquer zoom, o que permite ao texture() escolher um nível de mip e o que transforma uma posição numa normal.
Ao final você vai conseguir
- Dizer o que dFdx e dFdy retornam, e de onde vêm os números.
- Tirar o serrilhado de qualquer borda, linha ou padrão com fwidth, em qualquer zoom.
- Corrigir a linha que o atan ou o fract deixam numa textura.
- Montar uma normal plana a partir das derivadas de uma posição.
- Reconhecer onde as derivadas são indefinidas, e o que usar no lugar.
O rasterizador não entrega os pixels ao shader um por um. Ele os entrega em quadrados de 2 × 2, chamados quads, e os quatro pixels de um quad executam as mesmas instruções ao mesmo tempo. Em qualquer linha do shader, um pixel pode então ver o valor que o vizinho do quad acabou de calcular para a mesma expressão.
dFdx(v) é esse valor no pixel da direita menos o valor no pixel da esquerda. dFdy(v) é o pixel de cima menos o de baixo, já que o y cresce para cima no gl_FragCoord. Os dois pixels de um par recebem a mesma resposta. O fwidth soma os tamanhos das duas.
float w = fwidth(v); // = abs(dFdx(v)) + abs(dFdy(v))A unidade é sempre "por pixel". Se v é uma distância em metros, dFdx(v) é metros por pixel; se v é uma coordenada uv, é quanto da textura um pixel abrange. Para f = 0.1 · gl_FragCoord.x, dFdx(f) vale 0.1 em todo pixel e dFdy(f) vale 0. Quase todo este capítulo se resume a dividir por esse número para medir alguma coisa em pixels.
quadrados grossos = quads · o y cresce para cima
Todo pixel roda, então todo quad tem os seus quatro valores. Os dois pixels de um par compartilham uma diferença: o dFdx do pixel da esquerda e o do pixel da direita é o mesmo número.
a define o quão larga é a parábola e se ela abre para cima (a > 0) ou para baixo (a < 0). O vértice, o ponto de virada, fica em x = −b/(2a). As raízes são onde a curva encontra o eixo x; elas ficam simétricas em volta do vértice, √Δ/(2a) para cada lado. Deslize c para cima até a curva descolar do eixo: no exato instante em que Δ chega a 0 as duas raízes se fundem em uma, e além disso elas precisariam da raiz quadrada de um número negativo, então não há nenhuma. O ray tracing faz exatamente essa pergunta para um raio e uma esfera: duas raízes significam que o raio entra e sai, uma que ele raspa, nenhuma que ele erra.
Fine e coarse
Um quad tem duas linhas, então há dois valores possíveis de dFdx nele, um por linha. O GLSL 4.50 deixa você escolher: dFdxFine usa a linha do próprio pixel, dFdxCoarse pode usar uma linha para o quad inteiro, o que é mais barato. O dFdx simples deixa o driver escolher, e o GLSL ES e o WebGL só têm as versões simples. De qualquer jeito, uma derivada é uma diferença sobre um pixel, não uma inclinação exata: onde um valor faz uma curva fechada, ela erra em até um pixel.
Pixels auxiliares
A borda de um triângulo corta quads. Os pixels de um quad cortado que ficam fora do triângulo rodam o shader mesmo assim, como helper invocations, só para que os vizinhos tenham valores para subtrair. A cor deles é jogada fora (gl_HelperInvocation é true para eles, a partir do GLSL 4.50 e do ES 3.10). É por isso que uma malha feita de triângulos minúsculos custa muito mais do que a contagem de pixels sugere: um triângulo que cobre um pixel ainda roda quatro.
Formas em shaders normalmente são decididas por uma distância com sinal d, negativa dentro, como no capítulo Funções de distância com sinal. step(d, 0.0) faz a pergunta uma vez, no centro do pixel, e responde tudo ou nada. Um pixel com metade coberta pela forma recebe a cor inteira ou nenhuma, e as bordas viram escadinhas.
O que o pixel deveria receber é a fração dele que a forma cobre. Ao longo de um pixel, d muda mais ou menos fwidth(d), então d / fwidth(d) é a distância até a borda medida em pixels. Isso dá a cobertura em quatro passos:
1// Coverage of a filled shape: d < 0 inside
2float fillAA(float d) {
3 float w = fwidth(d); // scene units per pixel, here
4 return clamp(0.5 - d / w, 0.0, 1.0); // a one-pixel ramp centred on the edge
5}
6
7// Coverage of a line of half-width h, r = distance from its centre line.
8// A line thinner than a pixel is drawn one pixel wide and dimmed instead,
9// so it stays continuous and keeps the brightness it really has.
10float lineAA(float r, float h) {
11 float w = fwidth(r);
12 float wide = max(2.0 * h, w);
13 return clamp((0.5 * wide - abs(r)) / w + 0.5, 0.0, 1.0) * (2.0 * h / wide);
14}
15
16// Grid lines on a plane: past ~2 pixels per cell, fade to the average coverage
17float gridAA(vec2 uv, float h) {
18 vec2 g = abs(fract(uv + 0.5) - 0.5); // distance to the nearest line, per axis
19 vec2 w = fwidth(uv);
20 float cx = mix(lineAA(g.x, h), 2.0 * h, smoothstep(0.3, 0.7, w.x));
21 float cy = mix(lineAA(g.y, h), 2.0 * h, smoothstep(0.3, 0.7, w.y));
22 return 1.0 - (1.0 - cx) * (1.0 - cy); // either line covers the pixel
23}Cada pixel faz uma pergunta no seu centro, dentro ou fora, e recebe tudo ou nada. As bordas viram escadinhas, linhas mais finas que um pixel se quebram em tracinhos, e listras finas viram padrões que não existem (moiré).
A cena é desenhada numa resolução baixa e depois ampliada sem suavização, para você poder contar os pixels. Tamanho 1 é a resolução real da sua tela. Compare os quatro modos em cada cena, depois ligue "mover": uma imagem parada esconde a maior parte do serrilhado, o movimento o mostra.
Detalhes menores que um pixel
A rampa de um pixel resolve as bordas, mas dois tipos de detalhe precisam de mais. Uma linha mais fina que um pixel nunca cobre inteiramente o centro de um pixel, então a rampa simples a desenha como uma fileira quebrada de tracinhos que piscam quando ela se move. O lineAA acima a desenha com um pixel de largura e multiplica pela largura real em pixels, 2h / w: uma linha de um quarto de pixel vira uma linha de largura inteira com um quarto do brilho, que é o que o olho vê de uma linha fina de verdade.
Um padrão que se repete mais rápido que a cada dois pixels não tem como ser mostrado. Uma amostra por pixel não consegue distinguir listras de período 1,5 pixel de listras muito mais largas, e a tela mostra as largas, um moiré que não está na cena. A única imagem honesta é a cor média do padrão. O fwidth da coordenada do padrão diz quantos períodos cabem num pixel, e o shader passa para a média quando esse número se aproxima de meio. Os mipmaps fazem a mesma coisa com texturas, e é por isso que texturas não precisam desse código.
texture(s, uv) tira as derivadas do próprio uv. Multiplicadas pelo tamanho da textura, elas dizem quantos texels um pixel abrange, e o log na base 2 disso é o nível de mip. Um texel por pixel lê o nível 0; quatro texels por pixel leem o nível 2. O capítulo Mipmapping e compressão de texturas da trilha de OpenGL percorre a fórmula com números ao vivo.
Isso funciona enquanto o uv for suave. Coordenadas polares não são: o atan retorna −π de um lado do eixo x negativo e +π do outro, então u salta uma volta inteira entre dois pixels vizinhos. Dentro desse quad a derivada é enorme, o texture() conclui que o pixel abrange a textura inteira e lê o menor nível de mip, uma única cor média. O resultado é uma costura fina de um pixel. Repetir com fract(uv * 4.0) dentro de um atlas de texturas faz a mesma costura em toda borda de bloco.
Coordenadas de textura polares: u = ângulo, v = distância. À esquerda, u volta uma volta inteira (4 repetições da textura), as derivadas ali são enormes, o texture() escolhe o menor nível de mip e desenha uma linha fina e pontilhada de pixels cinza. A correção mantém a coordenada, mas dá ao textureGrad as derivadas de uma cópia cujo salto fica do outro lado.
A correção é manter a coordenada, mas dar à textura as derivadas que ela deveria ter tido. textureGrad(s, uv, dx, dy) as recebe como argumentos. Para um atlas repetido, elas são simplesmente as derivadas da coordenada antes do fract. Para coordenadas polares, calcule-as para duas cópias do ângulo com os saltos em lados opostos e use a menor: em qualquer pixel, pelo menos uma das duas é suave.
1vec2 tiled = uv * 4.0; // smooth: no jumps
2vec2 inTile = fract(tiled); // jumps from 1 back to 0 at every tile border
3vec2 atlasUV = uTileOrigin + inTile * uTileSize;
4
5// texture(uAtlas, atlasUV) would see the jump and draw a seam.
6// The derivatives of the smooth coordinate, scaled like atlasUV, are the right ones:
7vec4 c = textureGrad(uAtlas, atlasUV, dFdx(tiled) * uTileSize, dFdy(tiled) * uTileSize);Aplique dFdx e dFdy à posição da superfície no mundo. dFdx(vWorld) é o passo ao longo da superfície de um pixel para o próximo à direita, e dFdy(vWorld) o passo até o pixel de cima. Os dois estão na superfície, então o produto vetorial deles é perpendicular a ela: uma normal. Dentro de um triângulo a posição muda linearmente, então essa normal é constante por triângulo, o visual facetado do flat shading, sem vértices a mais. Ela também é sempre fiel à geometria realmente desenhada, mesmo depois que um vertex shader moveu os vértices e deixou as normais guardadas para trás.
vec3 N = normalize(cross(dFdx(vWorld), dFdy(vWorld)));O vertex shader empurra a esfera em calombos, mas o aNormal ainda guarda as normais da esfera, então as normais guardadas a iluminam como se fosse redonda. cross(dFdx(vWorld), dFdy(vWorld)) é montado a partir da superfície real, deslocada: sempre certa, uma normal por triângulo.
Uma derivada precisa que o vizinho tenha calculado a mesma expressão. Dentro de um if, de um laço ou de uma chamada de função cuja condição muda entre os pixels de um quad (fluxo de controle não uniforme), alguns vizinhos pularam aquele código, e a especificação chama o resultado de indefinido. Isso inclui as derivadas escondidas no texture() e no fwidth. Muitas GPUs por acaso dão uma resposta razoável mesmo assim, então o bug só aparece em parte do hardware, como borrões em blocos ou costuras ao longo da borda do desvio.
1// Undefined: texture() needs derivatives, and only some pixels of a quad get here
2if (noise(vWorld.xz) > 0.5) {
3 col = texture(uMoss, vUV).rgb;
4}
5
6// Defined: take the derivatives (or the sample) before the branch
7vec2 dx = dFdx(vUV), dy = dFdy(vUV);
8if (noise(vWorld.xz) > 0.5) {
9 col = textureGrad(uMoss, vUV, dx, dy).rgb; // or textureLod with a level you chose
10}| Situação | O que acontece | O que fazer |
|---|---|---|
| vertex, geometry ou compute shader | não há quads: dFdx não existe, e o texture() num vertex shader lê o nível 0 | textureLod com um nível explícito |
| dentro de um desvio que muda dentro de um quad | indefinido | calcule as derivadas antes do desvio; textureGrad ou textureLod dentro dele |
| um valor que salta (atan, fract, um hash por célula) | uma derivada enorme no salto: costuras, ou uma borda de um pixel no antisserrilhado | tire as derivadas de uma versão suave do valor |
| valores grandes, como posições no mundo longe da origem | a diferença de dois floats grandes quase iguais tem poucos dígitos corretos, e as normais ficam ruidosas | trabalhe em relação à câmera ou ao objeto |
| Sintoma | Causa | Correção |
|---|---|---|
| bordas duras com zoom longe, borradas com zoom perto | um smoothstep de largura fixa | faça a largura ser fwidth(d) |
| uma linha de um pixel ao longo de uma costura na textura | a coordenada salta dentro de um quad e escolhe o menor mip | textureGrad com as derivadas da coordenada suave |
| linhas finas viram tracinhos que piscam | linhas mais finas que um pixel cortadas com uma rampa de um pixel | desenhe com pelo menos um pixel de largura e escureça pela largura real |
| o chão distante mostra faixas curvas que não estão na cena | um padrão mais fino que dois pixels por período (moiré) | passe para a média onde o fwidth da coordenada se aproxima de ½ |
| uma normal plana aponta para dentro da superfície | o y aponta para baixo (Vulkan, ou um render target invertido) | inverta a ordem do produto vetorial |
| o dFdx dá 0 ou lixo só em alguns pixels | ele foi chamado dentro de um desvio que muda dentro do quad | mova-o para antes do desvio |
Ideias-chave
- Os pixels rodam em quads de 2×2; dFdx e dFdy são as diferenças entre vizinhos de um quad, em unidades por pixel.
- clamp(0.5 − d / fwidth(d), 0, 1) transforma qualquer distância com sinal numa borda de um pixel sem serrilhado, em qualquer zoom.
- Detalhes menores que um pixel precisam ser escurecidos (linhas finas) ou trocados pela média (padrões finos), nunca amostrados crus.
- O texture() escolhe o nível de mip a partir das derivadas do uv; uma coordenada que salta deixa uma costura, corrigida com textureGrad.
- cross(dFdx(p), dFdy(p)) é a normal plana verdadeira de qualquer coisa que foi desenhada.
- As derivadas são indefinidas dentro de desvios que mudam dentro de um quad, e não existem fora dos fragment shaders.